Browse Source

Merge branch 'master' of github.com:BlueWallet/BlueWallet into zigzag

# Conflicts:
#	package-lock.json
#	package.json
#	screen/lnd/manageFunds.js
zigzag
Marcos Rodriguez Vélez 6 years ago
parent
commit
c9c519bf2a
  1. 2
      .babelrc
  2. 6
      .buckconfig
  3. 73
      .flowconfig
  4. 1
      .gitattributes
  5. 59
      .gitignore
  6. 2
      .watchmanconfig
  7. 2
      App.js
  8. 48
      App.test.js
  9. 16
      App2.test.js
  10. 5
      BlueApp.js
  11. 755
      BlueComponents.js
  12. 195
      BlueElectrum.js
  13. 65
      Electrum.test.js
  14. 185
      HDWallet.test.js
  15. 86
      LightningCustodianWallet.test.js
  16. 49
      MainBottomTabs.js
  17. 21
      NavigationService.js
  18. 5
      README.md
  19. 18
      android/app/BUCK
  20. 100
      android/app/app.iml
  21. 51
      android/app/build.gradle
  22. 19
      android/app/build_defs.bzl
  23. 14
      android/app/src/main/AndroidManifest.xml
  24. BIN
      android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf
  25. BIN
      android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf
  26. BIN
      android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf
  27. 0
      android/app/src/main/java/io/bluewallet/bluewallet/MainActivity.java
  28. 35
      android/app/src/main/java/io/bluewallet/bluewallet/MainApplication.java
  29. 2
      android/app/src/main/res/values/strings.xml
  30. 23
      android/build.gradle
  31. 93
      android/build/intermediates/lint-cache/maven.google/com/android/support/group-index.xml
  32. 120
      android/build/intermediates/lint-cache/maven.google/master-index.xml
  33. 2
      android/gradle/wrapper/gradle-wrapper.properties
  34. 2
      android/metadata/en-US/full_description.txt
  35. 22
      android/settings.gradle
  36. 5
      app.json
  37. 3
      babel.config.js
  38. 77
      class/abstract-hd-wallet.js
  39. 15
      class/app-storage.js
  40. 66
      class/hd-legacy-breadwallet-wallet.js
  41. 105
      class/hd-legacy-p2pkh-wallet.js
  42. 170
      class/hd-segwit-p2sh-wallet.js
  43. 60
      class/lightning-custodian-wallet.js
  44. 79
      class/walletGradient.js
  45. 4
      events.js
  46. 6
      index.js
  47. 1278
      ios/BlueWallet.xcodeproj/project.pbxproj
  48. 2
      ios/BlueWallet/AppDelegate.h
  49. 17
      ios/BlueWallet/AppDelegate.m
  50. 10
      ios/BlueWallet/Info.plist
  51. 2
      ios/BlueWallet/main.m
  52. 2
      ios/BlueWalletTests/BlueWalletTests.m
  53. 2
      ios/BlueWalletTests/Info.plist
  54. 2
      ios/fastlane/metadata/en-US/description.txt
  55. 56
      ios/fastlane/metadata/en-US/release_notes.txt
  56. 2
      ios/fastlane/metadata/es-ES/description.txt
  57. 2
      ios/fastlane/metadata/pt-BR/description.txt
  58. 2
      ios/fastlane/metadata/pt-PT/description.txt
  59. 14
      loc/cs_CZ.js
  60. 14
      loc/da_DK.js
  61. 65
      loc/de_DE.js
  62. 16
      loc/en.js
  63. 14
      loc/es.js
  64. 14
      loc/fr_FR.js
  65. 54
      loc/hr_HR.js
  66. 220
      loc/id_ID.js
  67. 135
      loc/index.js
  68. 223
      loc/jp_JP.js
  69. 19
      loc/nl_NL.js
  70. 14
      loc/pt_BR.js
  71. 14
      loc/pt_PT.js
  72. 164
      loc/ru.js
  73. 22
      loc/th_TH.js
  74. 12
      loc/ua.js
  75. 215
      loc/zh_cn.js
  76. 8
      models/fiatUnit.js
  77. 2
      models/networkTransactionFees.js
  78. 13535
      package-lock.json
  79. 150
      package.json
  80. 1
      patches/fix_mangle.sh
  81. 16
      patches/minifier.js.patch
  82. 501
      screen/lnd/browser.js
  83. 17
      screen/lnd/lndCreateInvoice.js
  84. 53
      screen/lnd/lndViewAdditionalInvoiceInformation.js
  85. 177
      screen/lnd/lndViewInvoice.js
  86. 162
      screen/lnd/scanLndInvoice.js
  87. 54
      screen/receive/details.js
  88. 129
      screen/receive/receiveAmount.js
  89. 17
      screen/selftest.js
  90. 18
      screen/send/confirm.js
  91. 8
      screen/send/create.js
  92. 313
      screen/send/details.js
  93. 29
      screen/send/scanQrAddress.js
  94. 58
      screen/send/success.js
  95. 128
      screen/settings/about.js
  96. 2
      screen/settings/currency.js
  97. 9
      screen/settings/language.js
  98. 16
      screen/settings/lightningSettings.js
  99. 51
      screen/settings/releasenotes.js
  100. 2
      screen/settings/settings.js

2
.babelrc

@ -1,3 +1,3 @@
{
"presets": ["module:metro-react-native-babel-preset"]
}
}

6
.buckconfig

@ -0,0 +1,6 @@
[android]
target = Google Inc.:Google APIs:23
[maven_repositories]
central = https://repo1.maven.org/maven2

73
.flowconfig

@ -2,74 +2,69 @@
; We fork some components by platform
.*/*[.]android.js
; Ignore templates for 'react-native init'
<PROJECT_ROOT>/node_modules/react-native/local-cli/templates/.*
; Ignore RN jest
<PROJECT_ROOT>/node_modules/react-native/jest/.*
; Ignore RNTester
<PROJECT_ROOT>/node_modules/react-native/RNTester/.*
; Ignore the website subdir
<PROJECT_ROOT>/node_modules/react-native/website/.*
; Ignore the Dangerfile
<PROJECT_ROOT>/node_modules/react-native/danger/dangerfile.js
; Ignore Fbemitter
<PROJECT_ROOT>/node_modules/fbemitter/.*
; Ignore "BUCK" generated dirs
<PROJECT_ROOT>/node_modules/react-native/\.buckd/
<PROJECT_ROOT>/\.buckd/
; Ignore unexpected extra "@providesModule"
.*/node_modules/.*/node_modules/fbjs/.*
; Ignore duplicate module providers
; For RN Apps installed via npm, "Libraries" folder is inside
; "node_modules/react-native" but in the source repo it is in the root
.*/Libraries/react-native/React.js
; Ignore polyfills
<PROJECT_ROOT>/node_modules/react-native/Libraries/polyfills/.*
.*/Libraries/polyfills/.*
; Ignore various node_modules
<PROJECT_ROOT>/node_modules/react-native-gesture-handler/.*
<PROJECT_ROOT>/node_modules/expo/.*
<PROJECT_ROOT>/node_modules/react-navigation/.*
<PROJECT_ROOT>/node_modules/xdl/.*
<PROJECT_ROOT>/node_modules/reqwest/.*
<PROJECT_ROOT>/node_modules/metro-bundler/.*
; Ignore metro
.*/node_modules/metro/.*
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
node_modules/expo/flow/
node_modules/react-native/flow-github/
[options]
emoji=true
module.system=haste
esproposal.optional_chaining=enable
esproposal.nullish_coalescing=enable
module.file_ext=.js
module.file_ext=.jsx
module.file_ext=.json
module.file_ext=.ios.js
module.system=haste
module.system.haste.use_name_reducers=true
# get basename
module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1'
# strip .js or .js.flow suffix
module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1'
# strip .ios suffix
module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1'
module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1'
module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1'
module.system.haste.paths.blacklist=.*/__tests__/.*
module.system.haste.paths.blacklist=.*/__mocks__/.*
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/Animated/src/polyfills/.*
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/Libraries/.*
munge_underscores=true
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
module.file_ext=.js
module.file_ext=.jsx
module.file_ext=.json
module.file_ext=.native.js
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_type=$FixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
unsafe.enable_getters_and_setters=true
[version]
^0.56.0
^0.86.0

1
.gitattributes

@ -0,0 +1 @@
*.pbxproj -text

59
.gitignore

@ -1,29 +1,10 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
node_modules/
# misc
.env.local
.env.development.local
.env.test.local
.env.production.local
.idea/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
class/constants.js
# OSX# OSX
# OSX
#
.DS_Store
# Xcode
#
build/
ios/build/*
*.pbxuser
!default.pbxuser
*.mode1v3
@ -39,13 +20,41 @@ DerivedData
*.hmap
*.ipa
*.xcuserstate
#
project.xcworkspace
# Android
# Android/IntelliJ
#
android/local.properties
android/app/bluewallet-release-key.keystore
build/
.idea
.gradle
local.properties
*.iml
build/
# node.js
#
node_modules/
npm-debug.log
yarn-error.log
# BUCK
buck-out/
\.buckd/
*.keystore
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/
*/fastlane/report.xml
*/fastlane/Preview.html
*/fastlane/screenshots
# Bundle artifact
*.jsbundle
#BlueWallet
release-notes.json
release-notes.txt

2
.watchmanconfig

@ -1 +1 @@
{}
{}

2
App.js

@ -2,6 +2,7 @@ import React from 'react';
import { Linking } from 'react-native';
import { NavigationActions } from 'react-navigation';
import MainBottomTabs from './MainBottomTabs';
import NavigationService from './NavigationService';
export default class App extends React.Component {
navigator = null;
@ -53,6 +54,7 @@ export default class App extends React.Component {
<MainBottomTabs
ref={nav => {
this.navigator = nav;
NavigationService.setTopLevelNavigator(nav);
}}
/>
);

48
App.test.js

@ -9,7 +9,7 @@ import MockStorage from './MockStorage';
import { FiatUnit } from './models/fiatUnit';
global.crypto = require('crypto'); // shall be used by tests under nodejs CLI, but not in RN environment
let assert = require('assert');
jest.mock('react-native-custom-qr-codes', () => 'Video');
jest.mock('react-native-qrcode-svg', () => 'Video');
const AsyncStorage = new MockStorage();
jest.setMock('AsyncStorage', AsyncStorage);
jest.useFakeTimers();
@ -57,7 +57,7 @@ describe('unit - LegacyWallet', function() {
let b = LegacyWallet.fromJson(key);
assert(key === JSON.stringify(b));
assert.equal(key, JSON.stringify(b));
assert.strictEqual(key, JSON.stringify(b));
});
it('can validate addresses', () => {
@ -118,8 +118,8 @@ it('Appstorage - loadFromDisk works', async () => {
let Storage2 = new AppStorage();
await Storage2.loadFromDisk();
assert.equal(Storage2.wallets.length, 1);
assert.equal(Storage2.wallets[0].getLabel(), 'testlabel');
assert.strictEqual(Storage2.wallets.length, 1);
assert.strictEqual(Storage2.wallets[0].getLabel(), 'testlabel');
let isEncrypted = await Storage2.storageIsEncrypted();
assert.ok(!isEncrypted);
@ -146,7 +146,7 @@ it('Appstorage - encryptStorage & load encrypted storage works', async () => {
assert.ok(!isEncrypted);
await Storage.encryptStorage('password');
isEncrypted = await Storage.storageIsEncrypted();
assert.equal(Storage.cachedPassword, 'password');
assert.strictEqual(Storage.cachedPassword, 'password');
assert.ok(isEncrypted);
// saved, now trying to load, using good password
@ -156,8 +156,8 @@ it('Appstorage - encryptStorage & load encrypted storage works', async () => {
assert.ok(isEncrypted);
let loadResult = await Storage2.loadFromDisk('password');
assert.ok(loadResult);
assert.equal(Storage2.wallets.length, 1);
assert.equal(Storage2.wallets[0].getLabel(), 'testlabel');
assert.strictEqual(Storage2.wallets.length, 1);
assert.strictEqual(Storage2.wallets[0].getLabel(), 'testlabel');
// now trying to load, using bad password
@ -166,7 +166,7 @@ it('Appstorage - encryptStorage & load encrypted storage works', async () => {
assert.ok(isEncrypted);
loadResult = await Storage2.loadFromDisk('passwordBAD');
assert.ok(!loadResult);
assert.equal(Storage2.wallets.length, 0);
assert.strictEqual(Storage2.wallets.length, 0);
// now, trying case with adding data after decrypt.
// saveToDisk should be handled correctly
@ -176,14 +176,14 @@ it('Appstorage - encryptStorage & load encrypted storage works', async () => {
assert.ok(isEncrypted);
loadResult = await Storage2.loadFromDisk('password');
assert.ok(loadResult);
assert.equal(Storage2.wallets.length, 1);
assert.equal(Storage2.wallets[0].getLabel(), 'testlabel');
assert.strictEqual(Storage2.wallets.length, 1);
assert.strictEqual(Storage2.wallets[0].getLabel(), 'testlabel');
w = new SegwitP2SHWallet();
w.setLabel('testlabel2');
await w.generate();
Storage2.wallets.push(w);
assert.equal(Storage2.wallets.length, 2);
assert.equal(Storage2.wallets[1].getLabel(), 'testlabel2');
assert.strictEqual(Storage2.wallets.length, 2);
assert.strictEqual(Storage2.wallets[1].getLabel(), 'testlabel2');
await Storage2.saveToDisk();
// saved to encrypted storage after load. next load should be successfull
Storage2 = new AppStorage();
@ -191,15 +191,15 @@ it('Appstorage - encryptStorage & load encrypted storage works', async () => {
assert.ok(isEncrypted);
loadResult = await Storage2.loadFromDisk('password');
assert.ok(loadResult);
assert.equal(Storage2.wallets.length, 2);
assert.equal(Storage2.wallets[0].getLabel(), 'testlabel');
assert.equal(Storage2.wallets[1].getLabel(), 'testlabel2');
assert.strictEqual(Storage2.wallets.length, 2);
assert.strictEqual(Storage2.wallets[0].getLabel(), 'testlabel');
assert.strictEqual(Storage2.wallets[1].getLabel(), 'testlabel2');
// next, adding new `fake` storage which should be unlocked with `fake` password
let createFakeStorageResult = await Storage2.createFakeStorage('fakePassword');
assert.ok(createFakeStorageResult);
assert.equal(Storage2.wallets.length, 0);
assert.equal(Storage2.cachedPassword, 'fakePassword');
assert.strictEqual(Storage2.wallets.length, 0);
assert.strictEqual(Storage2.cachedPassword, 'fakePassword');
w = new SegwitP2SHWallet();
w.setLabel('fakewallet');
await w.generate();
@ -210,14 +210,14 @@ it('Appstorage - encryptStorage & load encrypted storage works', async () => {
let Storage3 = new AppStorage();
loadResult = await Storage3.loadFromDisk('password');
assert.ok(loadResult);
assert.equal(Storage3.wallets.length, 2);
assert.equal(Storage3.wallets[0].getLabel(), 'testlabel');
assert.strictEqual(Storage3.wallets.length, 2);
assert.strictEqual(Storage3.wallets[0].getLabel(), 'testlabel');
// fake:
Storage3 = new AppStorage();
loadResult = await Storage3.loadFromDisk('fakePassword');
assert.ok(loadResult);
assert.equal(Storage3.wallets.length, 1);
assert.equal(Storage3.wallets[0].getLabel(), 'fakewallet');
assert.strictEqual(Storage3.wallets.length, 1);
assert.strictEqual(Storage3.wallets[0].getLabel(), 'fakewallet');
});
it('Wallet can fetch UTXO', async () => {
@ -245,7 +245,7 @@ it('Wallet can fetch TXs', async () => {
let w = new LegacyWallet();
w._address = '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG';
await w.fetchTransactions();
assert.equal(w.getTransactions().length, 2);
assert.strictEqual(w.getTransactions().length, 2);
let tx0 = w.getTransactions()[0];
let txExpected = {
@ -296,7 +296,7 @@ it('Wallet can fetch TXs', async () => {
delete txExpected.confirmations;
delete tx0.preference; // that bs is not always the same
delete txExpected.preference;
assert.deepEqual(tx0, txExpected);
assert.deepStrictEqual(tx0, txExpected);
});
describe('currency', () => {
@ -321,7 +321,7 @@ describe('currency', () => {
await currency.setPrefferedCurrency(FiatUnit.EUR);
await currency.startUpdater();
let preferred = await currency.getPreferredCurrency();
assert.equal(preferred.endPointKey, 'EUR');
assert.strictEqual(preferred.endPointKey, 'EUR');
cur = JSON.parse(AsyncStorage.storageCache[AppStorage.EXCHANGE_RATES]);
assert.ok(cur['BTC_EUR'] > 0);
});

16
App2.test.js

@ -14,7 +14,10 @@ it('bip38 decodes', async () => {
{ N: 1, r: 8, p: 8 }, // using non-default parameters to speed it up (not-bip38 compliant)
);
assert.equal(wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed), '5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR');
assert.strictEqual(
wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed),
'5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR',
);
});
it('bip38 decodes slow', async () => {
@ -29,7 +32,10 @@ it('bip38 decodes slow', async () => {
let encryptedKey = '6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN';
let decryptedKey = await bip38.decrypt(encryptedKey, 'qwerty', status => process.stdout.write(parseInt(status.percent) + '%\r'));
assert.equal(wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed), 'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc');
assert.strictEqual(
wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed),
'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc',
);
});
describe('Watch only wallet', () => {
@ -46,16 +52,16 @@ describe('Watch only wallet', () => {
w.setSecret('167zK5iZrs1U6piDqubD3FjRqUTM2CZnb8');
await w.fetchTransactions();
assert.equal(w.getTransactions().length, 233);
assert.strictEqual(w.getTransactions().length, 233);
w = new WatchOnlyWallet();
w.setSecret('1BiJW1jyUaxcJp2JWwbPLPzB1toPNWTFJV');
await w.fetchTransactions();
assert.equal(w.getTransactions().length, 2);
assert.strictEqual(w.getTransactions().length, 2);
// fetch again and make sure no duplicates
await w.fetchTransactions();
assert.equal(w.getTransactions().length, 2);
assert.strictEqual(w.getTransactions().length, 2);
});
it('can fetch complex TXs', async () => {

5
BlueApp.js

@ -7,6 +7,7 @@ let EV = require('./events');
let currency = require('./currency');
let loc = require('./loc');
let A = require('./analytics');
let BlueElectrum = require('./BlueElectrum'); // eslint-disable-line
/** @type {AppStorage} */
let BlueApp = new AppStorage();
@ -31,7 +32,7 @@ async function startAndDecrypt(retry) {
let securityAlert = require('./security-alert');
await securityAlert.start();
// now, lets try to fetch balance and txs for first wallet if it is time for it
let hadToRefresh = false;
/* let hadToRefresh = false;
let noErr = true;
try {
let wallets = BlueApp.getWallets();
@ -57,7 +58,7 @@ async function startAndDecrypt(retry) {
if (hadToRefresh && noErr) {
await BlueApp.saveToDisk(); // caching
}
} */
}
if (!success && password) {

755
BlueComponents.js

@ -1,31 +1,36 @@
/* eslint react/prop-types: 0 */
/* global alert */
/** @type {AppStorage} */
import React, { Component } from 'react';
import Ionicons from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types';
import { Icon, Button, FormLabel, FormInput, Text, Header, List, ListItem } from 'react-native-elements';
import { Icon, FormLabel, FormInput, Text, Header, List, ListItem } from 'react-native-elements';
import {
TouchableOpacity,
TouchableWithoutFeedback,
Animated,
ActivityIndicator,
View,
UIManager,
StyleSheet,
Dimensions,
Image,
Keyboard,
SafeAreaView,
InputAccessoryView,
Clipboard,
Platform,
TextInput,
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import { WatchOnlyWallet, LegacyWallet } from './class';
import { LightningCustodianWallet } from './class';
import Carousel from 'react-native-snap-carousel';
import DeviceInfo from 'react-native-device-info';
import { HDLegacyP2PKHWallet } from './class/hd-legacy-p2pkh-wallet';
import { HDLegacyBreadwalletWallet } from './class/hd-legacy-breadwallet-wallet';
import { HDSegwitP2SHWallet } from './class/hd-segwit-p2sh-wallet';
import { LightningCustodianWallet } from './class/lightning-custodian-wallet';
import { BitcoinUnit } from './models/bitcoinUnits';
import NavigationService from './NavigationService';
import ImagePicker from 'react-native-image-picker';
import WalletGradient from './class/walletGradient';
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
let loc = require('./loc/');
/** @type {AppStorage} */
let BlueApp = require('./BlueApp');
@ -40,30 +45,34 @@ if (aspectRatio > 1.6) {
export class BlueButton extends Component {
render() {
// eslint-disable-next-line
this.props.buttonStyle = this.props.buttonStyle || {};
let backgroundColor = this.props.backgroundColor ? this.props.backgroundColor : '#ccddf9';
let fontColor = '#0c2550';
if (this.props.hasOwnProperty('disabled') && this.props.disabled === true) {
backgroundColor = '#eef0f4';
fontColor = '#9aa0aa';
}
return (
<Button
activeOpacity={0.1}
delayPressIn={0}
{...this.props}
<TouchableOpacity
style={{
flex: 1,
borderWidth: 0.7,
borderColor: 'transparent',
backgroundColor: backgroundColor,
minHeight: 45,
height: 45,
maxHeight: 45,
borderRadius: 25,
minWidth: width / 1.5,
justifyContent: 'center',
alignItems: 'center',
}}
buttonStyle={Object.assign(
{
backgroundColor: '#ccddf9',
minHeight: 45,
height: 45,
borderWidth: 0,
borderRadius: 25,
},
this.props.buttonStyle,
)}
color="#0c2550"
/>
{...this.props}
>
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
{this.props.icon && <Icon name={this.props.icon.name} type={this.props.icon.type} color={this.props.icon.color} />}
{this.props.title && <Text style={{ marginHorizontal: 8, fontSize: 16, color: fontColor }}>{this.props.title}</Text>}
</View>
</TouchableOpacity>
);
}
}
@ -106,7 +115,6 @@ export class LightningButton extends Component {
render() {
return (
<TouchableOpacity
disabled={this.props.disabled}
onPress={() => {
// eslint-disable-next-line
if (this.props.onPress) this.props.onPress();
@ -137,32 +145,20 @@ export class LightningButton extends Component {
}
}
LightningButton.propTypes = {
disabled: PropTypes.bool,
};
export class BlueButtonLink extends Component {
render() {
// eslint-disable-next-line
this.props.buttonStyle = this.props.buttonStyle || {};
return (
<Button
activeOpacity={0.1}
delayPressIn={0}
{...this.props}
<TouchableOpacity
style={{
marginTop: 20,
borderWidth: 0.7,
borderColor: 'transparent',
minHeight: 60,
minWidth: 100,
height: 60,
justifyContent: 'center',
}}
buttonStyle={{
height: 45,
width: width / 1.5,
}}
backgroundColor="transparent"
color="#0c2550"
/>
{...this.props}
>
<Text style={{ color: '#0c2550', textAlign: 'center', fontSize: 16 }}>{this.props.title}</Text>
</TouchableOpacity>
);
}
}
@ -181,7 +177,14 @@ export const BlueNavigationStyle = (navigation, withNavigationCloseButton = fals
headerRight: withNavigationCloseButton ? (
<TouchableOpacity
style={{ width: 40, height: 40, padding: 14 }}
onPress={customCloseButtonFunction === undefined ? () => navigation.goBack(null) : customCloseButtonFunction}
onPress={
customCloseButtonFunction === undefined
? () => {
Keyboard.dismiss();
navigation.goBack(null);
}
: customCloseButtonFunction
}
>
<Image style={{ alignSelf: 'center' }} source={require('./img/close.png')} />
</TouchableOpacity>
@ -197,6 +200,56 @@ export const BlueCopyToClipboardButton = ({ stringToCopy }) => {
);
};
export class BlueCopyTextToClipboard extends Component {
static propTypes = {
text: PropTypes.string,
};
static defaultProps = {
text: '',
};
constructor(props) {
super(props);
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
}
this.state = { hasTappedText: false, address: props.text };
}
copyToClipboard = () => {
this.setState({ hasTappedText: true }, () => {
Clipboard.setString(this.props.text);
this.setState({ address: loc.wallets.xpub.copiedToClipboard }, () => {
setTimeout(() => {
this.setState({ hasTappedText: false, address: this.props.text });
}, 1000);
});
});
};
render() {
return (
<View style={{ justifyContent: 'center', alignItems: 'center', paddingHorizontal: 16 }}>
<TouchableOpacity onPress={this.copyToClipboard} disabled={this.state.hasTappedText}>
<Animated.Text style={styleCopyTextToClipboard.address} numberOfLines={0}>
{this.state.address}
</Animated.Text>
</TouchableOpacity>
</View>
);
}
}
const styleCopyTextToClipboard = StyleSheet.create({
address: {
marginVertical: 32,
fontSize: 15,
color: '#9aa0aa',
textAlign: 'center',
},
});
export class SafeBlueArea extends Component {
render() {
return (
@ -219,13 +272,12 @@ export class BlueText extends Component {
render() {
return (
<Text
style={Object.assign(
{
color: BlueApp.settings.foregroundColor,
},
style={{
color: BlueApp.settings.foregroundColor,
// eslint-disable-next-line
this.props.style,
)}
...this.props.style,
}}
{...this.props}
/>
);
@ -500,6 +552,50 @@ export class BlueList extends Component {
}
}
export class BlueUseAllFundsButton extends Component {
static InputAccessoryViewID = 'useMaxInputAccessoryViewID';
static propTypes = {
wallet: PropTypes.shape().isRequired,
onUseAllPressed: PropTypes.func.isRequired,
};
render() {
return (
<InputAccessoryView nativeID={BlueUseAllFundsButton.InputAccessoryViewID}>
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ color: '#9aa0aa', fontSize: 16, marginHorizontal: 8 }}>
Total: {this.props.wallet.getBalance()} {BitcoinUnit.BTC}
</Text>
<BlueButtonLink title="Use All" onPress={this.props.onUseAllPressed} />
</View>
</InputAccessoryView>
);
}
}
export class BlueDismissKeyboardInputAccessory extends Component {
static InputAccessoryViewID = 'BlueDismissKeyboardInputAccessory';
render() {
return Platform.OS !== 'ios' ? null : (
<InputAccessoryView nativeID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}>
<View
style={{
backgroundColor: '#eef0f4',
height: 44,
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
<BlueButtonLink title="Done" onPress={Keyboard.dismiss} />
</View>
</InputAccessoryView>
);
}
}
export class BlueLoading extends Component {
render() {
return (
@ -525,7 +621,7 @@ const stylesBlueIcon = StyleSheet.create({
paddingHorizontal: 14,
paddingTop: 8,
},
boxIncomming: {
boxIncoming: {
position: 'relative',
},
ball: {
@ -534,14 +630,14 @@ const stylesBlueIcon = StyleSheet.create({
borderRadius: 15,
backgroundColor: '#ccddf9',
},
ballIncomming: {
ballIncoming: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: '#d2f8d6',
transform: [{ rotate: '-45deg' }],
},
ballIncommingWithoutRotate: {
ballIncomingWithoutRotate: {
width: 30,
height: 30,
borderRadius: 15,
@ -604,12 +700,12 @@ export class BluePlusIcon extends Component {
}
}
export class BlueTransactionIncommingIcon extends Component {
export class BlueTransactionIncomingIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.ballIncomming}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ballIncoming}>
<Icon {...this.props} name="arrow-down" size={16} type="font-awesome" color="#37c0a1" iconStyle={{ left: 0, top: 8 }} />
</View>
</View>
@ -622,7 +718,7 @@ export class BlueTransactionPendingIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ball}>
<Icon
{...this.props}
@ -643,7 +739,7 @@ export class BlueTransactionExpiredIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ballOutgoingWithoutRotate}>
<Icon {...this.props} name="hourglass-end" size={16} type="font-awesome" color="#d0021b" iconStyle={{ left: 0, top: 6 }} />
</View>
@ -657,8 +753,8 @@ export class BlueTransactionOnchainIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.ballIncomming}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ballIncoming}>
<Icon
{...this.props}
name="link"
@ -678,7 +774,7 @@ export class BlueTransactionOffchainIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ballOutgoingWithoutRotate}>
<Icon {...this.props} name="bolt" size={16} type="font-awesome" color="#d0021b" iconStyle={{ left: 0, top: 7 }} />
</View>
@ -692,8 +788,8 @@ export class BlueTransactionOffchainIncomingIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.ballIncommingWithoutRotate}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ballIncomingWithoutRotate}>
<Icon {...this.props} name="bolt" size={16} type="font-awesome" color="#37c0a1" iconStyle={{ left: 0, top: 7 }} />
</View>
</View>
@ -706,7 +802,7 @@ export class BlueTransactionOutgoingIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ballOutgoing}>
<Icon {...this.props} name="arrow-down" size={16} type="font-awesome" color="#d0021b" iconStyle={{ left: 0, top: 8 }} />
</View>
@ -887,7 +983,7 @@ export class NewWalletPanel extends Component {
style={{ marginVertical: 17 }}
>
<LinearGradient
colors={['#eef0f4', '#eef0f4']}
colors={WalletGradient.createWallet}
style={{
padding: 15,
borderRadius: 10,
@ -932,6 +1028,386 @@ export class NewWalletPanel extends Component {
}
}
export class BlueTransactionListItem extends Component {
static propTypes = {
item: PropTypes.shape().isRequired,
itemPriceUnit: PropTypes.string,
};
static defaultProps = {
itemPriceUnit: BitcoinUnit.BTC,
};
txMemo = () => {
if (BlueApp.tx_metadata[this.props.item.hash] && BlueApp.tx_metadata[this.props.item.hash]['memo']) {
return BlueApp.tx_metadata[this.props.item.hash]['memo'];
}
return '';
};
rowTitle = () => {
const item = this.props.item;
if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (isNaN(item.value)) {
item.value = '0';
}
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = item.timestamp + item.expire_time;
if (invoiceExpiration > now) {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString();
} else if (invoiceExpiration < now) {
if (item.ispaid) {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString();
} else {
return loc.lnd.expired;
}
}
} else {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString();
}
};
rowTitleStyle = () => {
const item = this.props.item;
let color = '#37c0a1';
if (item.type === 'user_invoice' || item.type === 'payment_request') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = item.timestamp + item.expire_time;
if (invoiceExpiration > now) {
color = '#37c0a1';
} else if (invoiceExpiration < now) {
if (item.ispaid) {
color = '#37c0a1';
} else {
color = '#FF0000';
}
}
} else if (item.value / 100000000 < 0) {
color = BlueApp.settings.foregroundColor;
}
return {
fontWeight: '600',
fontSize: 16,
color: color,
};
};
avatar = () => {
// is it lightning refill tx?
if (this.props.item.category === 'receive' && this.props.item.confirmations < 3) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
}
if (this.props.item.type && this.props.item.type === 'bitcoind_tx') {
return (
<View style={{ width: 25 }}>
<BlueTransactionOnchainIcon />
</View>
);
}
if (this.props.item.type === 'paid_invoice') {
// is it lightning offchain payment?
return (
<View style={{ width: 25 }}>
<BlueTransactionOffchainIcon />
</View>
);
}
if (this.props.item.type === 'user_invoice' || this.props.item.type === 'payment_request') {
if (!this.props.item.ispaid) {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = this.props.item.timestamp + this.props.item.expire_time;
if (invoiceExpiration < now) {
return (
<View style={{ width: 25 }}>
<BlueTransactionExpiredIcon />
</View>
);
}
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionOffchainIncomingIcon />
</View>
);
}
}
if (!this.props.item.confirmations) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
} else if (this.props.item.value < 0) {
return (
<View style={{ width: 25 }}>
<BlueTransactionOutgoingIcon />
</View>
);
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionIncomingIcon />
</View>
);
}
};
subtitle = () => {
return (
(this.props.item.confirmations < 7 ? loc.transactions.list.conf + ': ' + this.props.item.confirmations + ' ' : '') +
this.txMemo() +
(this.props.item.memo || '')
);
};
onPress = () => {
if (this.props.item.hash) {
NavigationService.navigate('TransactionDetails', { hash: this.props.item.hash });
} else if (
this.props.item.type === 'user_invoice' ||
this.props.item.type === 'payment_request' ||
this.props.item.type === 'paid_invoice'
) {
const lightningWallet = BlueApp.getWallets().filter(wallet => {
if (typeof wallet === 'object') {
if (wallet.hasOwnProperty('secret')) {
return wallet.getSecret() === this.props.item.fromWallet;
}
}
});
if (lightningWallet.length === 1) {
NavigationService.navigate('LNDViewInvoice', {
invoice: this.props.item,
fromWallet: lightningWallet[0],
isModal: false,
});
}
}
};
render() {
return (
<BlueListItem
avatar={this.avatar()}
title={loc.transactionTimeToReadable(this.props.item.received)}
subtitle={this.subtitle()}
onPress={this.onPress}
badge={{
value: 3,
textStyle: { color: 'orange' },
containerStyle: { marginTop: 0 },
}}
hideChevron
rightTitle={this.rowTitle()}
rightTitleStyle={this.rowTitleStyle()}
/>
);
}
}
export class BlueListTransactionItem extends Component {
static propTypes = {
item: PropTypes.shape().isRequired,
itemPriceUnit: PropTypes.string,
};
static defaultProps = {
itemPriceUnit: BitcoinUnit.BTC,
};
txMemo = () => {
if (BlueApp.tx_metadata[this.props.item.hash] && BlueApp.tx_metadata[this.props.item.hash]['memo']) {
return BlueApp.tx_metadata[this.props.item.hash]['memo'];
}
return '';
};
rowTitle = () => {
const item = this.props.item;
if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (isNaN(item.value)) {
item.value = '0';
}
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = item.timestamp + item.expire_time;
if (invoiceExpiration > now) {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString();
} else if (invoiceExpiration < now) {
if (item.ispaid) {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString();
} else {
return loc.lnd.expired;
}
}
} else {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString();
}
};
rowTitleStyle = () => {
const item = this.props.item;
let color = '#37c0a1';
if (item.type === 'user_invoice' || item.type === 'payment_request') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = item.timestamp + item.expire_time;
if (invoiceExpiration > now) {
color = '#37c0a1';
} else if (invoiceExpiration < now) {
if (item.ispaid) {
color = '#37c0a1';
} else {
color = '#FF0000';
}
}
} else if (item.value / 100000000 < 0) {
color = BlueApp.settings.foregroundColor;
}
return {
fontWeight: '600',
fontSize: 16,
color: color,
};
};
avatar = () => {
// is it lightning refill tx?
if (this.props.item.category === 'receive' && this.props.item.confirmations < 3) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
}
if (this.props.item.type && this.props.item.type === 'bitcoind_tx') {
return (
<View style={{ width: 25 }}>
<BlueTransactionOnchainIcon />
</View>
);
}
if (this.props.item.type === 'paid_invoice') {
// is it lightning offchain payment?
return (
<View style={{ width: 25 }}>
<BlueTransactionOffchainIcon />
</View>
);
}
if (this.props.item.type === 'user_invoice' || this.props.item.type === 'payment_request') {
if (!this.props.item.ispaid) {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = this.props.item.timestamp + this.props.item.expire_time;
if (invoiceExpiration < now) {
return (
<View style={{ width: 25 }}>
<BlueTransactionExpiredIcon />
</View>
);
}
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionOffchainIncomingIcon />
</View>
);
}
}
if (!this.props.item.confirmations) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
} else if (this.props.item.value < 0) {
return (
<View style={{ width: 25 }}>
<BlueTransactionOutgoingIcon />
</View>
);
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionIncomingIcon />
</View>
);
}
};
subtitle = () => {
return (
(this.props.item.confirmations < 7 ? loc.transactions.list.conf + ': ' + this.props.item.confirmations + ' ' : '') +
this.txMemo() +
(this.props.item.memo || '')
);
};
onPress = () => {
if (this.props.item.hash) {
NavigationService.navigate('TransactionDetails', { hash: this.props.item.hash });
} else if (
this.props.item.type === 'user_invoice' ||
this.props.item.type === 'payment_request' ||
this.props.item.type === 'paid_invoice'
) {
const lightningWallet = BlueApp.getWallets().filter(wallet => {
if (typeof wallet === 'object') {
if (wallet.hasOwnProperty('secret')) {
return wallet.getSecret() === this.props.item.fromWallet;
}
}
});
NavigationService.navigate('LNDViewInvoice', {
invoice: this.props.item,
fromWallet: lightningWallet[0],
isModal: false,
});
}
};
render() {
return (
<BlueListItem
avatar={this.avatar()}
title={loc.transactionTimeToReadable(this.props.item.received)}
subtitle={this.subtitle()}
onPress={this.onPress}
badge={{
value: 3,
textStyle: { color: 'orange' },
containerStyle: { marginTop: 0 },
}}
hideChevron
rightTitle={this.rowTitle()}
rightTitleStyle={this.rowTitleStyle()}
/>
);
}
}
const sliderWidth = width * 1;
const itemWidth = width * 0.82;
const sliderHeight = 190;
@ -948,12 +1424,17 @@ export class WalletsCarousel extends Component {
_renderItem({ item, index }) {
let scaleValue = new Animated.Value(1.0);
let props = { duration: 50 };
if (Platform.OS === 'android') {
props['useNativeDriver'] = true;
}
this.onPressedIn = () => {
Animated.spring(scaleValue, { toValue: 0.9, duration: 100, useNativeDriver: Platform.OS === 'android' }).start();
props.toValue = 0.9;
Animated.spring(scaleValue, props).start();
};
this.onPressedOut = () => {
Animated.spring(scaleValue, { toValue: 1.0, duration: 100, useNativeDriver: Platform.OS === 'android' }).start();
props.toValue = 1.0;
Animated.spring(scaleValue, props).start();
};
if (!item) {
@ -968,39 +1449,6 @@ export class WalletsCarousel extends Component {
);
}
let gradient1 = '#65ceef';
let gradient2 = '#68bbe1';
if (WatchOnlyWallet.type === item.type) {
gradient1 = '#7d7d7d';
gradient2 = '#4a4a4a';
}
if (LegacyWallet.type === item.type) {
gradient1 = '#40fad1';
gradient2 = '#15be98';
}
if (HDLegacyP2PKHWallet.type === item.type) {
gradient1 = '#e36dfa';
gradient2 = '#bd10e0';
}
if (HDLegacyBreadwalletWallet.type === item.type) {
gradient1 = '#fe6381';
gradient2 = '#f99c42';
}
if (HDSegwitP2SHWallet.type === item.type) {
gradient1 = '#c65afb';
gradient2 = '#9053fe';
}
if (LightningCustodianWallet.type === item.type) {
gradient1 = '#f1be07';
gradient2 = '#f79056';
}
return (
<Animated.View
style={{ paddingRight: 10, marginVertical: 17, transform: [{ scale: scaleValue }] }}
@ -1014,13 +1462,13 @@ export class WalletsCarousel extends Component {
onLongPress={WalletsCarousel.handleLongPress}
onPress={() => {
if (WalletsCarousel.handleClick) {
WalletsCarousel.handleClick(index, [gradient1, gradient2]);
WalletsCarousel.handleClick(index);
}
}}
>
<LinearGradient
shadowColor="#000000"
colors={[gradient1, gradient2]}
colors={WalletGradient.gradientsFor(item.type)}
style={{
padding: 15,
borderRadius: 10,
@ -1115,6 +1563,99 @@ export class WalletsCarousel extends Component {
}
}
export class BlueAddressInput extends Component {
static propTypes = {
isLoading: PropTypes.bool,
onChangeText: PropTypes.func,
onBarScanned: PropTypes.func,
address: PropTypes.string,
placeholder: PropTypes.string,
};
static defaultProps = {
isLoading: false,
address: '',
placeholder: loc.send.details.address,
};
render() {
return (
<View
style={{
flexDirection: 'row',
borderColor: '#d2d2d2',
borderBottomColor: '#d2d2d2',
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: '#f5f5f5',
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 8,
borderRadius: 4,
}}
>
<TextInput
onChangeText={text => {
this.props.onChangeText(text);
}}
placeholder={this.props.placeholder}
numberOfLines={1}
value={this.props.address}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
editable={!this.props.isLoading}
onSubmitEditing={Keyboard.dismiss}
{...this.props}
/>
<TouchableOpacity
disabled={this.props.isLoading}
onPress={() => {
Keyboard.dismiss();
ImagePicker.showImagePicker(
{
title: null,
mediaType: 'photo',
takePhotoButtonTitle: null,
customButtons: [{ name: 'navigatetoQRScan', title: 'Use Camera' }],
},
response => {
if (response.customButton) {
NavigationService.navigate('ScanQrAddress', { onBarScanned: this.props.onBarScanned });
} else if (response.uri) {
const uri = response.uri.toString().replace('file://', '');
LocalQRCode.decode(uri, (error, result) => {
if (!error) {
this.props.onBarScanned(result);
} else {
alert('The selected image does not contain a QR Code.');
}
});
}
},
);
}}
style={{
width: 75,
height: 36,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#bebebe',
borderRadius: 4,
paddingVertical: 4,
paddingHorizontal: 8,
marginHorizontal: 4,
}}
>
<Icon name="qrcode" size={22} type="font-awesome" color="#FFFFFF" />
<Text style={{ color: '#FFFFFF' }}>{loc.send.details.scan}</Text>
</TouchableOpacity>
</View>
);
}
}
export class BlueBitcoinAmount extends Component {
static propTypes = {
isLoading: PropTypes.bool,

195
BlueElectrum.js

@ -0,0 +1,195 @@
import { AsyncStorage } from 'react-native';
const ElectrumClient = require('electrum-client');
let bitcoin = require('bitcoinjs-lib');
let reverse = require('buffer-reverse');
const storageKey = 'ELECTRUM_PEERS';
const defaultPeer = { host: 'electrum.coinucopia.io', tcp: 50001 };
let mainClient = false;
let mainConnected = false;
async function connectMain() {
let usingPeer = await getRandomHardcodedPeer();
try {
console.log('begin connection:', JSON.stringify(usingPeer));
mainClient = new ElectrumClient(usingPeer.tcp, usingPeer.host, 'tcp');
await mainClient.connect();
const ver = await mainClient.server_version('2.7.11', '1.2');
console.log('connected to ', ver);
let peers = await mainClient.serverPeers_subscribe();
if (peers && peers.length > 0) {
mainConnected = true;
AsyncStorage.setItem(storageKey, JSON.stringify(peers));
}
} catch (e) {
mainConnected = false;
console.log('bad connection:', JSON.stringify(usingPeer));
}
if (!mainConnected) {
console.log('retry');
setTimeout(connectMain, 5000);
}
}
connectMain();
/**
* Returns random hardcoded electrum server guaranteed to work
* at the time of writing.
*
* @returns {Promise<{tcp, host}|*>}
*/
async function getRandomHardcodedPeer() {
let hardcodedPeers = [
{ host: 'node.ispol.sk', tcp: '50001' },
{ host: 'electrum.vom-stausee.de', tcp: '50001' },
{ host: 'orannis.com', tcp: '50001' },
{ host: '139.162.14.142', tcp: '50001' },
{ host: 'daedalus.bauerj.eu', tcp: '50001' },
{ host: 'electrum.eff.ro', tcp: '50001' },
{ host: 'electrum.anduck.net', tcp: '50001' },
{ host: 'mooo.not.fyi', tcp: '50011' },
{ host: 'electrum.coinucopia.io', tcp: '50001' },
];
return hardcodedPeers[(hardcodedPeers.length * Math.random()) | 0];
}
/**
* Returns random electrum server out of list of servers
* previous electrum server told us. Nearly half of them is
* usually offline.
* Not used for now.
*
* @returns {Promise<{tcp: number, host: string}>}
*/
// eslint-disable-next-line
async function getRandomDynamicPeer() {
try {
let peers = JSON.parse(await AsyncStorage.getItem(storageKey));
peers = peers.sort(() => Math.random() - 0.5); // shuffle
for (let peer of peers) {
let ret = {};
ret.host = peer[1];
for (let item of peer[2]) {
if (item.startsWith('t')) {
ret.tcp = item.replace('t', '');
}
}
if (ret.host && ret.tcp) return ret;
}
return defaultPeer; // failed to find random client, using default
} catch (_) {
return defaultPeer; // smth went wrong, using default
}
}
/**
*
* @param address {String}
* @returns {Promise<Object>}
*/
async function getBalanceByAddress(address) {
if (!mainClient) throw new Error('Electrum client is not connected');
let script = bitcoin.address.toOutputScript(address);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash));
let balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
balance.addr = address;
return balance;
}
/**
*
* @param address {String}
* @returns {Promise<Array>}
*/
async function getTransactionsByAddress(address) {
if (!mainClient) throw new Error('Electrum client is not connected');
let script = bitcoin.address.toOutputScript(address);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash));
let history = await mainClient.blockchainScripthash_getHistory(reversedHash.toString('hex'));
return history;
}
/**
*
* @param addresses {Array}
* @returns {Promise<{balance: number, unconfirmed_balance: number}>}
*/
async function multiGetBalanceByAddress(addresses) {
if (!mainClient) throw new Error('Electrum client is not connected');
let balance = 0;
let unconfirmedBalance = 0;
for (let addr of addresses) {
let b = await getBalanceByAddress(addr);
balance += b.confirmed;
unconfirmedBalance += b.unconfirmed_balance;
}
return { balance, unconfirmed_balance: unconfirmedBalance };
}
/**
* Simple waiter till `mainConnected` becomes true (which means
* it Electrum was connected in other function), or timeout 30 sec.
*
*
* @returns {Promise<Promise<*> | Promise<*>>}
*/
async function waitTillConnected() {
let waitTillConnectedInterval = false;
let retriesCounter = 0;
return new Promise(function(resolve, reject) {
waitTillConnectedInterval = setInterval(() => {
if (mainConnected) {
clearInterval(waitTillConnectedInterval);
resolve(true);
}
if (retriesCounter++ >= 30) {
clearInterval(waitTillConnectedInterval);
reject(new Error('Waiting for Electrum connection timeout'));
}
}, 1000);
});
}
module.exports.getBalanceByAddress = getBalanceByAddress;
module.exports.getTransactionsByAddress = getTransactionsByAddress;
module.exports.multiGetBalanceByAddress = multiGetBalanceByAddress;
module.exports.waitTillConnected = waitTillConnected;
module.exports.forceDisconnect = () => {
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
mainClient.close();
};
/*
let addr4elect = 'bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej';
let script = bitcoin.address.toOutputScript(addr4elect);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(hash.reverse());
console.log(addr4elect, ' maps to ', reversedHash.toString('hex'));
console.log(await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex')));
addr4elect = '1BWwXJH3q6PRsizBkSGm2Uw4Sz1urZ5sCj';
script = bitcoin.address.toOutputScript(addr4elect);
hash = bitcoin.crypto.sha256(script);
reversedHash = Buffer.from(hash.reverse());
console.log(addr4elect, ' maps to ', reversedHash.toString('hex'));
console.log(await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex')));
// let peers = await mainClient.serverPeers_subscribe();
// console.log(peers);
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
mainClient.close();
// setTimeout(()=>process.exit(), 3000); */

65
Electrum.test.js

@ -0,0 +1,65 @@
/* global it, describe, jasmine */
global.net = require('net');
let BlueElectrum = require('./BlueElectrum');
let assert = require('assert');
describe('Electrum', () => {
it('ElectrumClient can connect and query', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
const ElectrumClient = require('electrum-client');
let bitcoin = require('bitcoinjs-lib');
// let bitcoin = require('bitcoinjs-lib');
const peer = { host: 'electrum.coinucopia.io', ssl: 50002, tcp: 50001, pruning: null, http: null, https: null };
console.log('begin connection:', JSON.stringify(peer));
let mainClient = new ElectrumClient(peer.tcp, peer.host, 'tcp');
try {
await mainClient.connect();
const ver = await mainClient.server_version('2.7.11', '1.2');
console.log('connected to ', ver);
} catch (e) {
console.log('bad connection:', JSON.stringify(peer));
throw new Error();
}
let addr4elect = 'bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej';
let script = bitcoin.address.toOutputScript(addr4elect);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(hash.reverse());
let balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
assert.ok(balance.confirmed > 0);
addr4elect = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
script = bitcoin.address.toOutputScript(addr4elect);
hash = bitcoin.crypto.sha256(script);
reversedHash = Buffer.from(hash.reverse());
balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
assert.ok(balance.confirmed === 51432);
// let peers = await mainClient.serverPeers_subscribe();
// console.log(peers);
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
mainClient.close();
// setTimeout(()=>process.exit(), 3000);
});
it('BlueElectrum works', async function() {
let address = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
await BlueElectrum.waitTillConnected();
let balance = await BlueElectrum.getBalanceByAddress(address);
assert.strictEqual(balance.confirmed, 51432);
assert.strictEqual(balance.unconfirmed, 0);
assert.strictEqual(balance.addr, address);
let txs = await BlueElectrum.getTransactionsByAddress(address);
assert.strictEqual(txs.length, 1);
for (let tx of txs) {
assert.ok(tx.tx_hash);
assert.ok(tx.height);
}
BlueElectrum.forceDisconnect();
});
});

185
HDWallet.test.js

@ -1,18 +1,31 @@
/* global it, jasmine */
/* global it, jasmine, afterAll, beforeAll */
import { SegwitP2SHWallet, SegwitBech32Wallet, HDSegwitP2SHWallet, HDLegacyBreadwalletWallet, HDLegacyP2PKHWallet } from './class';
global.crypto = require('crypto'); // shall be used by tests under nodejs CLI, but not in RN environment
let assert = require('assert');
let bitcoin = require('bitcoinjs-lib');
global.net = require('net'); // needed by Electrum client. For RN it is proviced in shim.js
let BlueElectrum = require('./BlueElectrum'); // so it connects ASAP
afterAll(() => {
// after all tests we close socket so the test suite can actually terminate
return BlueElectrum.forceDisconnect();
});
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.waitTillConnected();
});
it('can convert witness to address', () => {
let address = SegwitP2SHWallet.witnessToAddress('035c618df829af694cb99e664ce1b34f80ad2c3b49bcd0d9c0b1836c66b2d25fd8');
assert.equal(address, '34ZVGb3gT8xMLT6fpqC6dNVqJtJmvdjbD7');
assert.strictEqual(address, '34ZVGb3gT8xMLT6fpqC6dNVqJtJmvdjbD7');
address = SegwitBech32Wallet.witnessToAddress('035c618df829af694cb99e664ce1b34f80ad2c3b49bcd0d9c0b1836c66b2d25fd8');
assert.equal(address, 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv');
assert.strictEqual(address, 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv');
address = SegwitBech32Wallet.scriptPubKeyToAddress('00144d757460da5fcaf84cc22f3847faaa1078e84f6a');
assert.equal(address, 'bc1qf46hgcx6tl90snxz9uuy0742zpuwsnm27ysdh7');
assert.strictEqual(address, 'bc1qf46hgcx6tl90snxz9uuy0742zpuwsnm27ysdh7');
});
it('can create a Segwit HD (BIP49)', async function() {
@ -21,39 +34,39 @@ it('can create a Segwit HD (BIP49)', async function() {
'honey risk juice trip orient galaxy win situate shoot anchor bounce remind horse traffic exotic since escape mimic ramp skin judge owner topple erode';
let hd = new HDSegwitP2SHWallet();
hd.setSecret(mnemonic);
assert.equal('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', hd._getExternalAddressByIndex(0));
assert.equal('35p5LwCAE7mH2css7onyQ1VuS1jgWtQ4U3', hd._getExternalAddressByIndex(1));
assert.equal('32yn5CdevZQLk3ckuZuA8fEKBco8mEkLei', hd._getInternalAddressByIndex(0));
assert.equal(true, hd.validateMnemonic());
assert.strictEqual('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', hd._getExternalAddressByIndex(0));
assert.strictEqual('35p5LwCAE7mH2css7onyQ1VuS1jgWtQ4U3', hd._getExternalAddressByIndex(1));
assert.strictEqual('32yn5CdevZQLk3ckuZuA8fEKBco8mEkLei', hd._getInternalAddressByIndex(0));
assert.strictEqual(true, hd.validateMnemonic());
await hd.fetchBalance();
assert.equal(hd.getBalance(), 0);
assert.strictEqual(hd.getBalance(), 0);
assert.ok(hd._lastTxFetch === 0);
await hd.fetchTransactions();
assert.ok(hd._lastTxFetch > 0);
assert.equal(hd.transactions.length, 4);
assert.strictEqual(hd.transactions.length, 4);
assert.equal('L4MqtwJm6hkbACLG4ho5DF8GhcXdLEbbvpJnbzA9abfD6RDpbr2m', hd._getExternalWIFByIndex(0));
assert.equal(
assert.strictEqual('L4MqtwJm6hkbACLG4ho5DF8GhcXdLEbbvpJnbzA9abfD6RDpbr2m', hd._getExternalWIFByIndex(0));
assert.strictEqual(
'ypub6WhHmKBmHNjcrUVNCa3sXduH9yxutMipDcwiKW31vWjcMbfhQHjXdyx4rqXbEtVgzdbhFJ5mZJWmfWwnP4Vjzx97admTUYKQt6b9D7jjSCp',
hd.getXpub(),
);
// checking that internal pointer and async address getter return the same address
let freeAddress = await hd.getAddressAsync();
assert.equal(hd._getExternalAddressByIndex(hd.next_free_address_index), freeAddress);
assert.strictEqual(hd._getExternalAddressByIndex(hd.next_free_address_index), freeAddress);
let freeChangeAddress = await hd.getChangeAddressAsync();
assert.equal(hd._getInternalAddressByIndex(hd.next_free_change_address_index), freeChangeAddress);
assert.strictEqual(hd._getInternalAddressByIndex(hd.next_free_change_address_index), freeChangeAddress);
});
it('Segwit HD (BIP49) can generate addressess only via ypub', async function() {
it('Segwit HD (BIP49) can generate addressess only via ypub', function() {
let ypub = 'ypub6WhHmKBmHNjcrUVNCa3sXduH9yxutMipDcwiKW31vWjcMbfhQHjXdyx4rqXbEtVgzdbhFJ5mZJWmfWwnP4Vjzx97admTUYKQt6b9D7jjSCp';
let hd = new HDSegwitP2SHWallet();
hd._xpub = ypub;
assert.equal('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', hd._getExternalAddressByIndex(0));
assert.equal('35p5LwCAE7mH2css7onyQ1VuS1jgWtQ4U3', hd._getExternalAddressByIndex(1));
assert.equal('32yn5CdevZQLk3ckuZuA8fEKBco8mEkLei', hd._getInternalAddressByIndex(0));
assert.strictEqual('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', hd._getExternalAddressByIndex(0));
assert.strictEqual('35p5LwCAE7mH2css7onyQ1VuS1jgWtQ4U3', hd._getExternalAddressByIndex(1));
assert.strictEqual('32yn5CdevZQLk3ckuZuA8fEKBco8mEkLei', hd._getInternalAddressByIndex(0));
});
it('can generate Segwit HD (BIP49)', async () => {
@ -88,56 +101,56 @@ it('HD (BIP49) can create TX', async () => {
await hd.getChangeAddressAsync(); // to refresh internal pointer to next free address
await hd.getAddressAsync(); // to refresh internal pointer to next free address
let txhex = hd.createTx(hd.utxo, 0.000014, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK');
assert.equal(
assert.strictEqual(
txhex,
'010000000001029d98d81fe2b596fd79e845fa9f38d7e0b6fb73303c40fac604d04df1fa137aee00000000171600142f18e8406c9d210f30c901b24e5feeae78784eb7ffffffff67fb86f310df24e508d40fce9511c7fde4dd4ee91305fd08a074279a70e2cd22000000001716001468dde644410cc789d91a7f36b823f38369755a1cffffffff02780500000000000017a914a3a65daca3064280ae072b9d6773c027b30abace87dc0500000000000017a914850f4dbc255654de2c12c6f6d79cf9cb756cad038702483045022100dc8390a9fd34c31259fa47f9fc182f20d991110ecfd5b58af1cf542fe8de257a022004c2d110da7b8c4127675beccc63b46fd65c706951f090fd381fa3b21d3c5c08012102edd141c5a27a726dda66be10a38b0fd3ccbb40e7c380034aaa43a1656d5f4dd60247304402207c0aef8313d55e72474247daad955979f62e56d1cbac5f2d14b8b022c6ce112602205d9aa3804f04624b12ab8a5ab0214b529c531c2f71c27c6f18aba6502a6ea0a80121030db3c49461a5e539e97bab62ab2b8f88151d1c2376493cf73ef1d02ef60637fd00000000',
);
txhex = hd.createTx(hd.utxo, 0.000005, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK');
var tx = bitcoin.Transaction.fromHex(txhex);
assert.equal(tx.ins.length, 1);
assert.equal(tx.outs.length, 2);
assert.equal(tx.outs[0].value, 500);
assert.equal(tx.outs[1].value, 400);
assert.strictEqual(tx.ins.length, 1);
assert.strictEqual(tx.outs.length, 2);
assert.strictEqual(tx.outs[0].value, 500);
assert.strictEqual(tx.outs[1].value, 400);
let chunksIn = bitcoin.script.decompile(tx.outs[0].script);
let toAddress = bitcoin.address.fromOutputScript(chunksIn);
chunksIn = bitcoin.script.decompile(tx.outs[1].script);
let changeAddress = bitcoin.address.fromOutputScript(chunksIn);
assert.equal('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', toAddress);
assert.equal(hd._getInternalAddressByIndex(hd.next_free_change_address_index), changeAddress);
assert.strictEqual('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', toAddress);
assert.strictEqual(hd._getInternalAddressByIndex(hd.next_free_change_address_index), changeAddress);
//
txhex = hd.createTx(hd.utxo, 0.000015, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK');
tx = bitcoin.Transaction.fromHex(txhex);
assert.equal(tx.ins.length, 2);
assert.equal(tx.outs.length, 2);
assert.strictEqual(tx.ins.length, 2);
assert.strictEqual(tx.outs.length, 2);
//
txhex = hd.createTx(hd.utxo, 0.00025, 0.00001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK');
tx = bitcoin.Transaction.fromHex(txhex);
assert.equal(tx.ins.length, 7);
assert.equal(tx.outs.length, 1);
assert.strictEqual(tx.ins.length, 7);
assert.strictEqual(tx.outs.length, 1);
chunksIn = bitcoin.script.decompile(tx.outs[0].script);
toAddress = bitcoin.address.fromOutputScript(chunksIn);
assert.equal('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', toAddress);
assert.strictEqual('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', toAddress);
// checking that change amount is at least 3x of fee, otherwise screw the change, just add it to fee.
// theres 0.00003 on UTXOs, lets transfer (0.00003 - 100sat), soo fee is equal to change (100 sat)
// which throws @dust error if broadcasted
txhex = hd.createTx(hd.utxo, 0.000028, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK');
tx = bitcoin.Transaction.fromHex(txhex);
assert.equal(tx.ins.length, 2);
assert.equal(tx.outs.length, 1); // only 1 output, which means change is neglected
assert.equal(tx.outs[0].value, 2800);
assert.strictEqual(tx.ins.length, 2);
assert.strictEqual(tx.outs.length, 1); // only 1 output, which means change is neglected
assert.strictEqual(tx.outs[0].value, 2800);
});
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, 11);
assert.strictEqual(hd.utxo.length, 11);
assert.ok(typeof hd.utxo[0].confirmations === 'number');
assert.ok(hd.utxo[0].txid);
assert.ok(hd.utxo[0].vout);
@ -148,6 +161,30 @@ it('Segwit HD (BIP49) can fetch UTXO', async function() {
);
});
it('Segwit HD (BIP49) can fetch balance with many used addresses in hierarchy', async function() {
if (!process.env.HD_MNEMONIC_BIP49_MANY_TX) {
console.error('process.env.HD_MNEMONIC_BIP49_MANY_TX not set, skipped');
return;
}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000;
let hd = new HDSegwitP2SHWallet();
hd.setSecret(process.env.HD_MNEMONIC_BIP49_MANY_TX);
assert.ok(hd.validateMnemonic());
let start = +new Date();
await hd.fetchBalance();
let end = +new Date();
const took = (end - start) / 1000;
took > 15 && console.warn('took', took, "sec to fetch huge HD wallet's balance");
assert.strictEqual(hd.getBalance(), 0.00051432);
await hd.fetchUtxo();
assert.ok(hd.utxo.length > 0);
await hd.fetchTransactions();
assert.strictEqual(hd.getTransactions().length, 107);
});
it('can work with malformed mnemonic', () => {
let mnemonic =
'honey risk juice trip orient galaxy win situate shoot anchor bounce remind horse traffic exotic since escape mimic ramp skin judge owner topple erode';
@ -168,7 +205,7 @@ it('can work with malformed mnemonic', () => {
hd = new HDSegwitP2SHWallet();
hd.setSecret(mnemonic);
let seed2 = hd.getMnemonicToSeedHex();
assert.equal(seed1, seed2);
assert.strictEqual(seed1, seed2);
assert.ok(hd.validateMnemonic());
});
@ -182,28 +219,28 @@ it('can create a Legacy HD (BIP44)', async function() {
let mnemonic = process.env.HD_MNEMONIC_BREAD;
let hd = new HDLegacyP2PKHWallet();
hd.setSecret(mnemonic);
assert.equal(hd.validateMnemonic(), true);
assert.equal(hd._getExternalAddressByIndex(0), '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG');
assert.equal(hd._getExternalAddressByIndex(1), '1QDCFcpnrZ4yrAQxmbvSgeUC9iZZ8ehcR5');
assert.equal(hd._getInternalAddressByIndex(0), '1KZjqYHm7a1DjhjcdcjfQvYfF2h6PqatjX');
assert.equal(hd._getInternalAddressByIndex(1), '13CW9WWBsWpDUvLtbFqYziWBWTYUoQb4nU');
assert.equal(
assert.strictEqual(hd.validateMnemonic(), true);
assert.strictEqual(hd._getExternalAddressByIndex(0), '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG');
assert.strictEqual(hd._getExternalAddressByIndex(1), '1QDCFcpnrZ4yrAQxmbvSgeUC9iZZ8ehcR5');
assert.strictEqual(hd._getInternalAddressByIndex(0), '1KZjqYHm7a1DjhjcdcjfQvYfF2h6PqatjX');
assert.strictEqual(hd._getInternalAddressByIndex(1), '13CW9WWBsWpDUvLtbFqYziWBWTYUoQb4nU');
assert.strictEqual(
hd.getXpub(),
'xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps',
);
assert.equal(hd._getExternalWIFByIndex(0), 'L1hqNoJ26YuCdujMBJfWBNfgf4Jo7AcKFvcNcKLoMtoJDdDtRq7Q');
assert.equal(hd._getExternalWIFByIndex(1), 'KyyH4h59iatJWwFfiYPnYkw39SP7cBwydC3xzszsBBXHpfwz9cKb');
assert.equal(hd._getInternalWIFByIndex(0), 'Kx3QkrfemEEV49Mj5oWfb4bsWymboPdstta7eN3kAzop9apxYEFP');
assert.equal(hd._getInternalWIFByIndex(1), 'Kwfg1EDjFapN9hgwafdNPEH22z3vkd4gtG785vXXjJ6uvVWAJGtr');
assert.strictEqual(hd._getExternalWIFByIndex(0), 'L1hqNoJ26YuCdujMBJfWBNfgf4Jo7AcKFvcNcKLoMtoJDdDtRq7Q');
assert.strictEqual(hd._getExternalWIFByIndex(1), 'KyyH4h59iatJWwFfiYPnYkw39SP7cBwydC3xzszsBBXHpfwz9cKb');
assert.strictEqual(hd._getInternalWIFByIndex(0), 'Kx3QkrfemEEV49Mj5oWfb4bsWymboPdstta7eN3kAzop9apxYEFP');
assert.strictEqual(hd._getInternalWIFByIndex(1), 'Kwfg1EDjFapN9hgwafdNPEH22z3vkd4gtG785vXXjJ6uvVWAJGtr');
await hd.fetchBalance();
assert.equal(hd.balance, 0);
assert.strictEqual(hd.balance, 0);
assert.ok(hd._lastTxFetch === 0);
await hd.fetchTransactions();
assert.ok(hd._lastTxFetch > 0);
assert.equal(hd.transactions.length, 4);
assert.equal(hd.next_free_address_index, 1);
assert.equal(hd.next_free_change_address_index, 1);
assert.strictEqual(hd.transactions.length, 4);
assert.strictEqual(hd.next_free_address_index, 1);
assert.strictEqual(hd.next_free_change_address_index, 1);
for (let tx of hd.getTransactions()) {
assert.ok(tx.value === 1000 || tx.value === 1377 || tx.value === -1377 || tx.value === -1000);
@ -211,17 +248,17 @@ it('can create a Legacy HD (BIP44)', async function() {
// checking that internal pointer and async address getter return the same address
let freeAddress = await hd.getAddressAsync();
assert.equal(hd._getExternalAddressByIndex(hd.next_free_address_index), freeAddress);
assert.strictEqual(hd._getExternalAddressByIndex(hd.next_free_address_index), freeAddress);
});
it('Legacy HD (BIP44) can generate addressess based on xpub', async function() {
let xpub = 'xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps';
let hd = new HDLegacyP2PKHWallet();
hd._xpub = xpub;
assert.equal(hd._getExternalAddressByIndex(0), '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG');
assert.equal(hd._getInternalAddressByIndex(0), '1KZjqYHm7a1DjhjcdcjfQvYfF2h6PqatjX');
assert.equal(hd._getExternalAddressByIndex(1), '1QDCFcpnrZ4yrAQxmbvSgeUC9iZZ8ehcR5');
assert.equal(hd._getInternalAddressByIndex(1), '13CW9WWBsWpDUvLtbFqYziWBWTYUoQb4nU');
assert.strictEqual(hd._getExternalAddressByIndex(0), '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG');
assert.strictEqual(hd._getInternalAddressByIndex(0), '1KZjqYHm7a1DjhjcdcjfQvYfF2h6PqatjX');
assert.strictEqual(hd._getExternalAddressByIndex(1), '1QDCFcpnrZ4yrAQxmbvSgeUC9iZZ8ehcR5');
assert.strictEqual(hd._getInternalAddressByIndex(1), '13CW9WWBsWpDUvLtbFqYziWBWTYUoQb4nU');
});
it('Legacy HD (BIP44) can create TX', async () => {
@ -239,38 +276,38 @@ it('Legacy HD (BIP44) can create TX', async () => {
await hd.getAddressAsync(); // to refresh internal pointer to next free address
let txhex = hd.createTx(hd.utxo, 0.0008, 0.000005, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK');
assert.equal(
assert.strictEqual(
txhex,
'01000000045fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f030000006b4830450221009be5dbe37db5a8409ddce3570140c95d162a07651b1e48cf39a6a741892adc53022061a25b8024d8f3cb1b94f264245de0c6e9a103ea557ddeb66245b40ec8e9384b012102ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f000000006a47304402207106e9fa4e2e35d351fbccc9c0fad3356d85d0cd35a9d7e9cbcefce5440da1e5022073c1905b5927447378c0f660e62900c1d4b2691730799458889fb87d86f5159101210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f020000006a4730440220250b15094096c4d4fe6793da8e45fa118ed057cc2759a480c115e76e23590791022079cdbdc9e630d713395602071e2837ecc1d192a36a24d8ec71bc51d5e62b203b01210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f010000006b483045022100879da610e6ed12c84d55f12baf3bf6222d59b5282502b3c7f4db1d22152c16900220759a1c88583cbdaf7fde21c273ad985dfdf94a2fa85e42ee41dcea2fd69136fd012102ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002ffffffff02803801000000000017a914a3a65daca3064280ae072b9d6773c027b30abace872c4c0000000000001976a9146ee5e3e66dc73587a3a2d77a1a6c8554fae21b8a88ac00000000',
);
var tx = bitcoin.Transaction.fromHex(txhex);
assert.equal(tx.ins.length, 4);
assert.equal(tx.outs.length, 2);
assert.equal(tx.outs[0].value, 80000); // payee
assert.equal(tx.outs[1].value, 19500); // change
assert.strictEqual(tx.ins.length, 4);
assert.strictEqual(tx.outs.length, 2);
assert.strictEqual(tx.outs[0].value, 80000); // payee
assert.strictEqual(tx.outs[1].value, 19500); // change
let chunksIn = bitcoin.script.decompile(tx.outs[0].script);
let toAddress = bitcoin.address.fromOutputScript(chunksIn);
chunksIn = bitcoin.script.decompile(tx.outs[1].script);
let changeAddress = bitcoin.address.fromOutputScript(chunksIn);
assert.equal('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', toAddress);
assert.equal(hd._getInternalAddressByIndex(hd.next_free_change_address_index), changeAddress);
assert.strictEqual('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', toAddress);
assert.strictEqual(hd._getInternalAddressByIndex(hd.next_free_change_address_index), changeAddress);
// checking that change amount is at least 3x of fee, otherwise screw the change, just add it to fee.
// theres 0.001 on UTXOs, lets transfer (0.001 - 100sat), soo fee is equal to change (100 sat)
// which throws @dust error if broadcasted
txhex = hd.createTx(hd.utxo, 0.000998, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK');
tx = bitcoin.Transaction.fromHex(txhex);
assert.equal(tx.ins.length, 4);
assert.equal(tx.outs.length, 1); // only 1 output, which means change is neglected
assert.equal(tx.outs[0].value, 99800);
assert.strictEqual(tx.ins.length, 4);
assert.strictEqual(tx.outs.length, 1); // only 1 output, which means change is neglected
assert.strictEqual(tx.outs[0].value, 99800);
});
it('Legacy HD (BIP44) can fetch UTXO', async function() {
let hd = new HDLegacyP2PKHWallet();
hd.usedAddresses = ['1Ez69SnzzmePmZX3WpEzMKTrcBF2gpNQ55', '1BiTCHeYzJNMxBLFCMkwYXNdFEdPJP53ZV']; // hacking internals
await hd.fetchUtxo();
assert.equal(hd.utxo.length, 11);
assert.strictEqual(hd.utxo.length, 11);
assert.ok(typeof hd.utxo[0].confirmations === 'number');
assert.ok(hd.utxo[0].txid);
assert.ok(hd.utxo[0].vout);
@ -290,31 +327,31 @@ it('HD breadwallet works', async function() {
let hdBread = new HDLegacyBreadwalletWallet();
hdBread.setSecret(process.env.HD_MNEMONIC_BREAD);
assert.equal(hdBread.validateMnemonic(), true);
assert.equal(hdBread._getExternalAddressByIndex(0), '1ARGkNMdsBE36fJhddSwf8PqBXG3s4d2KU');
assert.equal(hdBread._getInternalAddressByIndex(0), '1JLvA5D7RpWgChb4A5sFcLNrfxYbyZdw3V');
assert.strictEqual(hdBread.validateMnemonic(), true);
assert.strictEqual(hdBread._getExternalAddressByIndex(0), '1ARGkNMdsBE36fJhddSwf8PqBXG3s4d2KU');
assert.strictEqual(hdBread._getInternalAddressByIndex(0), '1JLvA5D7RpWgChb4A5sFcLNrfxYbyZdw3V');
assert.equal(
assert.strictEqual(
hdBread.getXpub(),
'xpub68nLLEi3KERQY7jyznC9PQSpSjmekrEmN8324YRCXayMXaavbdEJsK4gEcX2bNf9vGzT4xRks9utZ7ot1CTHLtdyCn9udvv1NWvtY7HXroh',
);
await hdBread.fetchBalance();
assert.equal(hdBread.balance, 0);
assert.strictEqual(hdBread.balance, 0);
assert.ok(hdBread._lastTxFetch === 0);
await hdBread.fetchTransactions();
assert.ok(hdBread._lastTxFetch > 0);
assert.equal(hdBread.transactions.length, 177);
assert.strictEqual(hdBread.transactions.length, 177);
for (let tx of hdBread.getTransactions()) {
assert.ok(tx.confirmations);
}
assert.equal(hdBread.next_free_address_index, 10);
assert.equal(hdBread.next_free_change_address_index, 118);
assert.strictEqual(hdBread.next_free_address_index, 10);
assert.strictEqual(hdBread.next_free_change_address_index, 118);
// checking that internal pointer and async address getter return the same address
let freeAddress = await hdBread.getAddressAsync();
assert.equal(hdBread._getExternalAddressByIndex(hdBread.next_free_address_index), freeAddress);
assert.strictEqual(hdBread._getExternalAddressByIndex(hdBread.next_free_address_index), freeAddress);
});
it('can convert blockchain.info TX to blockcypher TX format', () => {

86
LightningCustodianWallet.test.js

@ -152,7 +152,7 @@ describe('LightningCustodianWallet', () => {
}
await l2.fetchTransactions();
assert.equal(l2.transactions_raw.length, txLen + 1);
assert.strictEqual(l2.transactions_raw.length, txLen + 1);
// transactions became more after paying an invoice
// now, trying to pay duplicate invoice
@ -165,7 +165,7 @@ describe('LightningCustodianWallet', () => {
}
assert.ok(caughtError);
await l2.fetchTransactions();
assert.equal(l2.transactions_raw.length, txLen + 1);
assert.strictEqual(l2.transactions_raw.length, txLen + 1);
// havent changed since last time
end = +new Date();
if ((end - start) / 1000 > 9) {
@ -191,21 +191,21 @@ describe('LightningCustodianWallet', () => {
await lNew.createAccount(true);
await lNew.authorize();
await lNew.fetchBalance();
assert.equal(lNew.balance, 0);
assert.strictEqual(lNew.balance, 0);
let invoices = await lNew.getUserInvoices();
let invoice = await lNew.addInvoice(1, 'test memo');
let invoices2 = await lNew.getUserInvoices();
assert.equal(invoices2.length, invoices.length + 1);
assert.strictEqual(invoices2.length, invoices.length + 1);
assert.ok(invoices2[0].ispaid === false);
assert.ok(invoices2[0].description);
assert.equal(invoices2[0].description, 'test memo');
assert.strictEqual(invoices2[0].description, 'test memo');
assert.ok(invoices2[0].payment_request);
assert.ok(invoices2[0].timestamp);
assert.ok(invoices2[0].expire_time);
assert.equal(invoices2[0].amt, 1);
assert.strictEqual(invoices2[0].amt, 1);
for (let inv of invoices2) {
assert.equal(inv.type, 'user_invoice');
assert.strictEqual(inv.type, 'user_invoice');
}
await lOld.fetchBalance();
@ -225,11 +225,11 @@ describe('LightningCustodianWallet', () => {
await lOld.fetchBalance();
await lNew.fetchBalance();
assert.equal(oldBalance - lOld.balance, 1);
assert.equal(lNew.balance, 1);
assert.strictEqual(oldBalance - lOld.balance, 1);
assert.strictEqual(lNew.balance, 1);
await lOld.fetchTransactions();
assert.equal(lOld.transactions_raw.length, txLen + 1, 'internal invoice should also produce record in payer`s tx list');
assert.strictEqual(lOld.transactions_raw.length, txLen + 1, 'internal invoice should also produce record in payer`s tx list');
let newTx = lOld.transactions_raw.slice().pop();
assert.ok(typeof newTx.fee !== 'undefined');
assert.ok(newTx.value);
@ -244,8 +244,33 @@ describe('LightningCustodianWallet', () => {
await lNew.payInvoice(invoice);
await lOld.fetchBalance();
await lNew.fetchBalance();
assert.equal(lOld.balance - oldBalance, 1);
assert.equal(lNew.balance, 0);
assert.strictEqual(lOld.balance - oldBalance, 1);
assert.strictEqual(lNew.balance, 0);
// now, paying same internal invoice. should fail:
let coughtError = false;
await lOld.fetchTransactions();
txLen = lOld.transactions_raw.length;
let invLen = (await lNew.getUserInvoices()).length;
try {
await lOld.payInvoice(invoice);
} catch (Err) {
coughtError = true;
}
assert.ok(coughtError);
await lOld.fetchTransactions();
assert.strictEqual(txLen, lOld.transactions_raw.length, 'tx count should not be changed');
assert.strictEqual(invLen, (await lNew.getUserInvoices()).length, 'invoices count should not be changed');
// testing how limiting works:
assert.strictEqual(lNew.user_invoices_raw.length, 1);
await lNew.addInvoice(666, 'test memo 2');
invoices = await lNew.getUserInvoices(1);
assert.strictEqual(invoices.length, 2);
assert.strictEqual(invoices[0].amt, 1);
assert.strictEqual(invoices[1].amt, 666);
});
it('can pay free amount (tip) invoice', async function() {
@ -294,7 +319,7 @@ describe('LightningCustodianWallet', () => {
let decoded = await l2.decodeInvoice(invoice);
assert.ok(decoded.payment_hash);
assert.ok(decoded.description);
assert.equal(+decoded.num_satoshis, 0);
assert.strictEqual(+decoded.num_satoshis, 0);
await l2.checkRouteInvoice(invoice);
@ -317,10 +342,41 @@ describe('LightningCustodianWallet', () => {
}
await l2.fetchTransactions();
assert.equal(l2.transactions_raw.length, txLen + 1);
assert.strictEqual(l2.transactions_raw.length, txLen + 1);
// transactions became more after paying an invoice
await l2.fetchBalance();
assert.equal(oldBalance - l2.balance, 3);
assert.strictEqual(oldBalance - l2.balance, 3);
});
it('cant pay negative free amount', async () => {
let l1 = new LightningCustodianWallet();
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(true);
await l1.authorize();
await l1.fetchBalance();
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.balance === 0);
let invoice = await l1.addInvoice(0, 'zero amt inv');
let error = false;
try {
await l1.payInvoice(invoice, -1);
} catch (Err) {
error = true;
}
await l1.fetchBalance();
assert.strictEqual(l1.balance, 0);
assert.ok(error);
});
});

49
MainBottomTabs.js

@ -2,6 +2,7 @@ import { createStackNavigator, createAppContainer } from 'react-navigation';
import Settings from './screen/settings/settings';
import About from './screen/settings/about';
import ReleaseNotes from './screen/settings/releasenotes';
import Selftest from './screen/selftest';
import Language from './screen/settings/language';
import Currency from './screen/settings/currency';
@ -35,6 +36,7 @@ import Success from './screen/send/success';
import ManageFunds from './screen/lnd/manageFunds';
import ScanLndInvoice from './screen/lnd/scanLndInvoice';
import LappBrowser from './screen/lnd/browser';
import LNDCreateInvoice from './screen/lnd/lndCreateInvoice';
import LNDZigZag from './screen/lnd/lndZigZag';
import LNDViewInvoice from './screen/lnd/lndViewInvoice';
@ -86,6 +88,10 @@ const WalletsStackNavigator = createStackNavigator(
screen: About,
path: 'About',
},
ReleaseNotes: {
screen: ReleaseNotes,
path: 'ReleaseNotes',
},
Selftest: {
screen: Selftest,
},
@ -105,6 +111,14 @@ const WalletsStackNavigator = createStackNavigator(
screen: LightningSettings,
path: 'LightningSettings',
},
LNDViewInvoice: {
screen: LNDViewInvoice,
swipeEnabled: false,
gesturesEnabled: false,
},
LNDViewAdditionalInvoiceInformation: {
screen: LNDViewAdditionalInvoiceInformation,
},
},
{ headerBackTitleVisible: false },
);
@ -152,17 +166,6 @@ const ManageFundsStackNavigator = createStackNavigator({
},
});
const LNDViewInvoiceStackNavigator = createStackNavigator({
LNDViewInvoice: {
screen: LNDViewInvoice,
swipeEnabled: false,
gesturesEnabled: false,
},
LNDViewAdditionalInvoiceInformation: {
screen: LNDViewAdditionalInvoiceInformation,
},
});
const LNDCreateInvoiceStackNavigator = createStackNavigator({
LNDCreateInvoice: {
screen: LNDCreateInvoice,
@ -186,6 +189,15 @@ const CreateWalletStackNavigator = createStackNavigator({
},
});
const LightningScanInvoiceStackNavigator = createStackNavigator({
ScanLndInvoice: {
screen: ScanLndInvoice,
},
Success: {
screen: Success,
},
});
const MainBottomTabs = createStackNavigator(
{
Wallets: {
@ -242,11 +254,18 @@ const MainBottomTabs = createStackNavigator(
},
},
ScanLndInvoice: {
screen: ScanLndInvoice,
screen: LightningScanInvoiceStackNavigator,
navigationOptions: {
header: null,
},
},
ScanQrAddress: {
screen: sendScanQrAddress,
},
LappBrowser: {
screen: LappBrowser,
},
ReorderWallets: {
screen: ReorderWalletsStackNavigator,
navigationOptions: {
@ -259,12 +278,6 @@ const MainBottomTabs = createStackNavigator(
header: null,
},
},
LNDViewExistingInvoice: {
screen: LNDViewInvoiceStackNavigator,
navigationOptions: {
header: null,
},
},
},
{
mode: 'modal',

21
NavigationService.js

@ -0,0 +1,21 @@
import { NavigationActions } from 'react-navigation';
let _navigator;
function setTopLevelNavigator(navigatorRef) {
_navigator = navigatorRef;
}
function navigate(routeName, params) {
_navigator.dispatch(
NavigationActions.navigate({
routeName,
params,
}),
);
}
export default {
navigate,
setTopLevelNavigator,
};

5
README.md

@ -64,3 +64,8 @@ MIT
Grab an issue from [the backlog](https://github.com/BlueWallet/BlueWallet/projects/1), try to start or submit a PR, any doubts we will try to guide you.
Join us at our [telegram group](https://t.me/bluewallet) where we hangout :+1:
## Responsible disclosure
Found critical bugs/vulnerabilities? Please email them bluewallet@bluewallet.io
Thanks!

18
android/app/BUCK

@ -8,23 +8,13 @@
# - `buck install -r android/app` - compile, install and run application
#
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
lib_deps = []
for jarfile in glob(['libs/*.jar']):
name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')]
lib_deps.append(':' + name)
prebuilt_jar(
name = name,
binary_jar = jarfile,
)
create_aar_targets(glob(["libs/*.aar"]))
for aarfile in glob(['libs/*.aar']):
name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')]
lib_deps.append(':' + name)
android_prebuilt_aar(
name = name,
aar = aarfile,
)
create_jar_targets(glob(["libs/*.jar"]))
android_library(
name = "all-libs",

100
android/app/app.iml

@ -23,12 +23,11 @@
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7">
<output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/test/debug" />
<output url="file://$MODULE_DIR$/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes" />
<output-test url="file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/compileDebugUnitTestJavaWithJavac/classes" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
@ -36,7 +35,6 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" />
@ -85,18 +83,50 @@
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/generated/not_namespaced_r_class_sources" />
<excludeFolder url="file://$MODULE_DIR$/build/generated/source/r" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/annotation_processor_list" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/apk_list" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/build-info" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/builds" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/check-libraries" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/check-manifest" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/checkDebugClasspath" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/checkReleaseClasspath" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/compatible_screen_manifest" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-runtime-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-verifier" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant-run-apk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_run_main_apk_resources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_run_merged_manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_run_split_apk_resources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaPrecompile" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javac" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/legacy_multidex_aapt_derived_proguard_rules" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/legacy_multidex_main_dex_list" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/linked_res_for_bundle" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint_jar" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifest-checker" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/merged_assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/merged_manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/module_bundle" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/prebuild" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/processed_res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/reload-dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/resources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shader_assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/split-apk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/split_list" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/splits-support" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
@ -105,59 +135,53 @@
</content>
<orderEntry type="jdk" jdkName="Android API 27 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:fbcore-1.10.0" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:fresco-1.10.0" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:drawee-1.10.0" level="project" />
<orderEntry type="library" name="Gradle: org.webkit:android-jsc:r174650@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-vector-drawable:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:fresco:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.react:react-native:0.57.8@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:livedata-core:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-core-utils:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:runtime:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:fbcore:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:common:1.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-okhttp3-1.10.0" level="project" />
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-analytics-impl-16.0.5" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-annotations:27.1.1@jar" level="project" />
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-tasks-16.0.1" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:animated-vector-drawable-27.1.1" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:viewmodel:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:drawee:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.squareup.okhttp3:okhttp:3.11.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-ads-identifier-16.0.0" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-compat-27.1.1" level="project" />
<orderEntry type="library" name="Gradle: com.parse.bolts:bolts-tasks:1.4.0@jar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:viewmodel-1.1.0" level="project" />
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-measurement-base-16.0.4" level="project" />
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-analytics-16.0.5" level="project" />
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-stats-16.0.1" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:exifinterface:28.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-v4:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.core:runtime:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.squareup.okhttp3:okhttp-urlconnection:3.11.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-vector-drawable-27.1.1" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-core-ui-27.1.1" level="project" />
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-basement-16.0.1" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-media-compat-26.1.0" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-core-utils-27.1.1" level="project" />
<orderEntry type="library" name="Gradle: org.webkit:android-jsc-r174650" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-v4-26.1.0" level="project" />
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-tagmanager-v4-impl-16.0.5" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:animated-vector-drawable:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-okhttp3:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-base:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: io.sentry:sentry:1.7.5@jar" level="project" />
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-base-16.0.1" level="project" />
<orderEntry type="library" name="Gradle: com.squareup.okio:okio:1.14.0@jar" level="project" />
<orderEntry type="library" name="Gradle: javax.inject:javax.inject:1@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-fragment-27.1.1" level="project" />
<orderEntry type="library" name="Gradle: android.arch.core:runtime-1.1.0" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.soloader:soloader-0.5.1" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.react:react-native-0.57.5" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-core-ui:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-compat:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.infer.annotation:infer-annotation:0.11.2@jar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline:1.10.0@aar" level="project" />
<orderEntry type="library" name="Gradle: org.slf4j:slf4j-api:1.7.24@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:appcompat-v7-27.1.1" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-media-compat:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: com.google.code.findbugs:jsr305:3.0.2@jar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:livedata-core-1.1.0" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-fragment:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: io.sentry:sentry-android:1.7.5@jar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-1.10.0" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:support-annotations:28.0.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.android.support:appcompat-v7:27.1.1@aar" level="project" />
<orderEntry type="library" name="Gradle: android.arch.core:common:1.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: com.fasterxml.jackson.core:jackson-core:2.8.7@jar" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-base-1.10.0" level="project" />
<orderEntry type="library" name="Gradle: android.arch.lifecycle:runtime-1.1.0" level="project" />
<orderEntry type="library" name="Gradle: com.facebook.soloader:soloader:0.5.1@aar" level="project" />
<orderEntry type="module" module-name="react-native-webview" />
<orderEntry type="module" module-name="react-native-linear-gradient" />
<orderEntry type="module" module-name="react-native-svg" />
<orderEntry type="module" module-name="react-native-sentry" />
<orderEntry type="module" module-name="react-native-google-analytics-bridge" />
<orderEntry type="module" module-name="react-native-haptic-feedback" />
<orderEntry type="module" module-name="react-native-gesture-handler" />
<orderEntry type="module" module-name="react-native-fs" />
<orderEntry type="module" module-name="react-native-prompt-android" />
<orderEntry type="module" module-name="react-native-linear-gradient" />
<orderEntry type="module" module-name="react-native-vector-icons" />
<orderEntry type="module" module-name="react-native-svg" />
<orderEntry type="module" module-name="react-native-device-info" />
<orderEntry type="module" module-name="react-native-randombytes" />
<orderEntry type="module" module-name="react-native-camera" />

51
android/app/build.gradle

@ -77,7 +77,6 @@ project.ext.react = [
]
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
/**
* Set this to true to create two separate APKs instead of one:
@ -102,21 +101,12 @@ android {
applicationId "io.bluewallet.bluewallet"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 15
versionName "3.5.5"
versionCode 1
versionName "3.8.0"
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
signingConfigs {
release {
if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
storeFile file(MYAPP_RELEASE_STORE_FILE)
storePassword MYAPP_RELEASE_STORE_PASSWORD
keyAlias MYAPP_RELEASE_KEY_ALIAS
keyPassword MYAPP_RELEASE_KEY_PASSWORD
}
}
multiDexEnabled true
}
splits {
abi {
@ -130,7 +120,6 @@ android {
release {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
signingConfig signingConfigs.release
}
}
// applicationVariants are e.g. debug, release
@ -138,7 +127,7 @@ android {
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
def versionCodes = ["armeabi-v7a":1, "x86":2]
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
@ -149,24 +138,26 @@ android {
}
dependencies {
compile project(':react-native-camera')
compile project(':react-native-fs')
compile project(':react-native-gesture-handler')
compile project(':react-native-vector-icons')
compile project(':react-native-svg')
compile project(':react-native-sentry')
compile project(':react-native-randombytes')
compile project(':react-native-prompt-android')
compile project(':react-native-linear-gradient')
compile project(':react-native-haptic-feedback')
compile project(':react-native-google-analytics-bridge')
compile project(':react-native-device-info')
implementation (project(':react-native-camera')) {
exclude group: "com.android.support"
}
implementation project(':react-native-tcp')
implementation project(':@remobile_react-native-qrcode-local-image')
implementation project(':react-native-image-picker')
implementation project(':react-native-webview')
implementation project(':react-native-svg')
implementation project(':react-native-vector-icons')
implementation project(':react-native-sentry')
implementation project(':react-native-randombytes')
implementation project(':react-native-prompt-android')
implementation project(':react-native-linear-gradient')
implementation project(':react-native-haptic-feedback')
implementation project(':react-native-google-analytics-bridge')
implementation project(':react-native-gesture-handler')
implementation project(':react-native-fs')
implementation project(':react-native-device-info')
implementation project(':react-native-camera')
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation "com.facebook.react:react-native:+" // From node_modules
implementation 'com.android.support:multidex:1.0.3'
}
// Run this once to be able to run the application with BUCK

19
android/app/build_defs.bzl

@ -0,0 +1,19 @@
"""Helper definitions to glob .aar and .jar targets"""
def create_aar_targets(aarfiles):
for aarfile in aarfiles:
name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
lib_deps.append(":" + name)
android_prebuilt_aar(
name = name,
aar = aarfile,
)
def create_jar_targets(jarfiles):
for jarfile in jarfiles:
name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
lib_deps.append(":" + name)
prebuilt_jar(
name = name,
binary_jar = jarfile,
)

14
android/app/src/main/AndroidManifest.xml

@ -3,12 +3,12 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CAMERA"/>
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme">
<activity
@ -21,11 +21,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bitcoin" />
<data android:scheme="lightning" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bitcoin" />
<data android:scheme="lightning" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

BIN
android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf

Binary file not shown.

BIN
android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf

Binary file not shown.

BIN
android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf

Binary file not shown.

0
android/app/src/main/java/com/bluewallet/MainActivity.java → android/app/src/main/java/io/bluewallet/bluewallet/MainActivity.java

35
android/app/src/main/java/com/bluewallet/MainApplication.java → android/app/src/main/java/io/bluewallet/bluewallet/MainApplication.java

@ -3,6 +3,27 @@ package io.bluewallet.bluewallet;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.peel.react.TcpSocketsModule;
import com.remobile.qrcodeLocalImage.RCTQRCodeLocalImagePackage;
import com.imagepicker.ImagePickerPackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import io.sentry.RNSentryPackage;
import com.bitgo.randombytes.RandomBytesPackage;
import im.shimo.react.prompt.RNPromptPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.mkuczera.RNReactNativeHapticFeedbackPackage;
import com.idehub.GoogleAnalyticsBridge.GoogleAnalyticsBridgePackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.rnfs.RNFSPackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import org.reactnative.camera.RNCameraPackage;
import io.sentry.RNSentryPackage;
import com.bitgo.randombytes.RandomBytesPackage;
import im.shimo.react.prompt.RNPromptPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.mkuczera.RNReactNativeHapticFeedbackPackage;
import com.idehub.GoogleAnalyticsBridge.GoogleAnalyticsBridgePackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.horcrux.svg.SvgPackage;
import io.sentry.RNSentryPackage;
@ -35,17 +56,21 @@ public class MainApplication extends Application implements ReactApplication {
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new RNFSPackage() ,
new VectorIconsPackage(),
new SvgPackage(),
new TcpSocketsModule(),
new RCTQRCodeLocalImagePackage(),
new ImagePickerPackage(),
new RNCWebViewPackage(),
new RNSentryPackage(),
new RandomBytesPackage(),
new RNPromptPackage(),
new LinearGradientPackage(),
new RNReactNativeHapticFeedbackPackage(),
new GoogleAnalyticsBridgePackage(),
new RNDeviceInfo(),
new RNCameraPackage(),
new LinearGradientPackage(),
new RNFSPackage() ,
new VectorIconsPackage(),
new SvgPackage(),
new RNCameraPackage(),
new RNGestureHandlerPackage()
);
}

2
android/app/src/main/res/values/strings.xml

@ -1,3 +1,3 @@
<resources>
<string name="app_name">Blue Wallet</string>
<string name="app_name">BlueWallet</string>
</resources>

23
android/build.gradle

@ -2,18 +2,18 @@
buildscript {
ext {
buildToolsVersion = "27.0.3"
buildToolsVersion = "28.0.2"
minSdkVersion = 16
compileSdkVersion = 27
targetSdkVersion = 26
supportLibVersion = "27.1.1"
compileSdkVersion = 28
targetSdkVersion = 27
supportLibVersion = "28.0.0"
}
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.android.tools.build:gradle:3.2.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -34,17 +34,6 @@ allprojects {
task wrapper(type: Wrapper) {
gradleVersion = '4.4'
gradleVersion = '4.7'
distributionUrl = distributionUrl.replace("bin", "all")
}
subprojects {
project.configurations.all {
resolutionStrategy.eachDependency { details ->
if (details.requested.group == 'com.android.support'
&& !details.requested.name.contains('multidex') ) {
details.useVersion "26.1.0"
}
}
}
}

93
android/build/intermediates/lint-cache/maven.google/com/android/support/group-index.xml

@ -1,93 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<com.android.support>
<support-compat versions="24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<leanback-v17 versions="21.0.0,21.0.2,21.0.3,22.0.0,22.1.0,22.1.1,22.2.0,22.2.1,23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<recommendation versions="23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-tv-provider versions="26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-vector-drawable versions="23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<recyclerview-v7 versions="21.0.0,21.0.2,21.0.3,22.0.0,22.1.0,22.1.1,22.2.0,22.2.1,23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<preference-leanback-v17 versions="23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<preference-v14 versions="23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<percent versions="22.2.0,23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-media-compat versions="24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<cardview-v7 versions="21.0.0,21.0.2,21.0.3,22.0.0,22.1.0,22.1.1,22.2.0,22.2.1,23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<wearable versions="26.0.0-alpha1"/>
<exifinterface versions="25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-annotations versions="19.1.0,20.0.0,21.0.0,21.0.2,21.0.3,22.0.0,22.1.0,22.1.1,22.2.0,22.2.1,23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<appcompat-v7 versions="18.0.0,19.0.0,19.0.1,19.1.0,20.0.0,21.0.0,21.0.2,21.0.3,22.0.0,22.1.0,22.1.1,22.2.0,22.2.1,23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<palette-v7 versions="21.0.0,21.0.2,21.0.3,22.0.0,22.1.0,22.1.1,22.2.0,22.2.1,23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<multidex-instrumentation versions="1.0.0,1.0.1,1.0.2,1.0.3"/>
<multidex versions="1.0.0,1.0.1,1.0.2,1.0.3"/>
<mediarouter-v7 versions="18.0.0,19.0.0,19.0.1,19.1.0,20.0.0,21.0.0,21.0.2,21.0.3,22.0.0,22.1.0,22.1.1,22.2.0,22.2.1,23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-alpha4,28.0.0-alpha5,28.0.0-beta01,28.0.0"/>
<preference-v7 versions="23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-dynamic-animation versions="25.3.0,25.3.1,25.4.0,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-fragment versions="24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<design versions="22.2.0,22.2.1,23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<transition versions="24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<customtabs versions="23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-core-ui versions="24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<gridlayout-v7 versions="13.0.0,18.0.0,19.0.0,19.0.1,19.1.0,20.0.0,21.0.0,21.0.2,21.0.3,22.0.0,22.1.0,22.1.1,22.2.0,22.2.1,23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<animated-vector-drawable versions="23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-core-utils versions="24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-v13 versions="13.0.0,18.0.0,19.0.0,19.0.1,19.1.0,20.0.0,21.0.0,21.0.2,21.0.3,22.0.0,22.1.0,22.1.1,22.2.0,22.2.1,23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<instantvideo versions="26.0.0-alpha1"/>
<support-v4 versions="13.0.0,18.0.0,19.0.0,19.0.1,19.1.0,20.0.0,21.0.0,21.0.2,21.0.3,22.0.0,22.1.0,22.1.1,22.2.0,22.2.1,23.0.0,23.0.1,23.1.0,23.1.1,23.2.0,23.2.1,23.3.0,23.4.0,24.0.0-alpha1,24.0.0-alpha2,24.0.0-beta1,24.0.0,24.1.0,24.1.1,24.2.0,24.2.1,25.0.0,25.0.1,25.1.0,25.1.1,25.2.0,25.3.0,25.3.1,25.4.0,26.0.0-alpha1,26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-emoji versions="26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<wear versions="26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-emoji-appcompat versions="26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-emoji-bundled versions="26.0.0-beta1,26.0.0-beta2,26.0.0,26.0.1,26.0.2,26.1.0,27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<support-content versions="27.0.0,27.0.1,27.0.2,27.1.0,27.1.1,28.0.0-alpha1"/>
<design-bottomnavigation versions="28.0.0-alpha1"/>
<design-button versions="28.0.0-alpha1"/>
<design-circularreveal-cardview versions="28.0.0-alpha1"/>
<design-bottomappbar versions="28.0.0-alpha1"/>
<design-card versions="28.0.0-alpha1"/>
<design-shape versions="28.0.0-alpha1"/>
<design-drawable versions="28.0.0-alpha1"/>
<design-bottomsheet versions="28.0.0-alpha1"/>
<design-floatingactionbutton versions="28.0.0-alpha1"/>
<design-circularreveal-coordinatorlayout versions="28.0.0-alpha1"/>
<design-textfield versions="28.0.0-alpha1"/>
<design-stateful versions="28.0.0-alpha1"/>
<design-circularreveal versions="28.0.0-alpha1"/>
<design-expandable versions="28.0.0-alpha1"/>
<design-navigation versions="28.0.0-alpha1"/>
<design-dialog versions="28.0.0-alpha1"/>
<design-canvas versions="28.0.0-alpha1"/>
<design-tabs versions="28.0.0-alpha1"/>
<design-chip versions="28.0.0-alpha1"/>
<design-snackbar versions="28.0.0-alpha1"/>
<design-theme versions="28.0.0-alpha1"/>
<design-math versions="28.0.0-alpha1"/>
<design-transformation versions="28.0.0-alpha1"/>
<design-widget versions="28.0.0-alpha1"/>
<design-animation versions="28.0.0-alpha1"/>
<design-typography versions="28.0.0-alpha1"/>
<design-color versions="28.0.0-alpha1"/>
<design-internal versions="28.0.0-alpha1"/>
<design-resources versions="28.0.0-alpha1"/>
<design-ripple versions="28.0.0-alpha1"/>
<coordinatorlayout versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<collections versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<slidingpanelayout versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<asynclayoutinflater versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<slices-view versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<recyclerview-selection versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<viewpager versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<cursoradapter versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<localbroadcastmanager versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<heifwriter versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<customview versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<print versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<slices-builders versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<interpolator versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<slices-core versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<loader versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<swiperefreshlayout versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<drawerlayout versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<documentfile versions="28.0.0-alpha1,28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<webkit versions="28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<car versions="28.0.0-alpha3,28.0.0-alpha4,28.0.0-alpha5"/>
<versionedparcelable versions="28.0.0-alpha3,28.0.0-beta01,28.0.0-rc01,28.0.0-rc02,28.0.0"/>
<media2 versions="28.0.0-alpha01,28.0.0-alpha02,28.0.0-alpha03"/>
</com.android.support>

120
android/build/intermediates/lint-cache/maven.google/master-index.xml

@ -1,120 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<com.android.support.constraint/>
<com.android.databinding/>
<com.android.support/>
<com.android.support.test/>
<com.android.support.test.janktesthelper/>
<com.android.support.test.uiautomator/>
<com.android.support.test.espresso/>
<android.arch.persistence.room/>
<android.arch.lifecycle/>
<android.arch.core/>
<com.google.android.instantapps/>
<com.google.android.instantapps.thirdpartycompat/>
<com.android.java.tools.build/>
<com.android.tools/>
<com.android.tools.layoutlib/>
<com.android.tools.ddms/>
<com.android.tools.external.com-intellij/>
<com.android.tools.build/>
<com.android.tools.analytics-library/>
<com.android.tools.internal.build.test/>
<com.android.tools.lint/>
<com.android.tools.external.org-jetbrains/>
<com.android.support.test.espresso.idling/>
<com.android.support.test.services/>
<com.google.firebase/>
<com.google.android.gms/>
<com.google.gms/>
<android.arch.paging/>
<com.crashlytics.sdk.android/>
<io.fabric.sdk.android/>
<android.arch.persistence/>
<com.google.android.wearable/>
<com.google.android.support/>
<com.android.installreferrer/>
<com.google.ar/>
<androidx.core/>
<com.google.android.things/>
<com.android.tools.build.jetifier/>
<tools.base.build-system.debug/>
<androidx.databinding/>
<androidx.constraintlayout/>
<org.chromium.net/>
<com.google.android.play/>
<androidx.multidex/>
<com.google.android.material/>
<androidx.test.services/>
<androidx.test.janktesthelper/>
<androidx.test/>
<androidx.test.espresso/>
<androidx.test.espresso.idling/>
<androidx.test.uiautomator/>
<androidx.room/>
<androidx.paging/>
<androidx.lifecycle/>
<androidx.sqlite/>
<androidx.arch.core/>
<android.arch.work/>
<android.arch.navigation/>
<androidx.mediarouter/>
<androidx.percentlayout/>
<androidx.emoji/>
<androidx.cardview/>
<androidx.preference/>
<androidx.wear/>
<androidx.legacy/>
<androidx.documentfile/>
<androidx.car/>
<androidx.swiperefreshlayout/>
<androidx.leanback/>
<androidx.appcompat/>
<androidx.customview/>
<androidx.gridlayout/>
<androidx.vectordrawable/>
<androidx.heifwriter/>
<androidx.transition/>
<androidx.print/>
<androidx.viewpager/>
<androidx.annotation/>
<androidx.exifinterface/>
<androidx.dynamicanimation/>
<androidx.browser/>
<androidx.localbroadcastmanager/>
<androidx.asynclayoutinflater/>
<androidx.contentpager/>
<androidx.slidingpanelayout/>
<androidx.cursoradapter/>
<androidx.media/>
<androidx.loader/>
<androidx.interpolator/>
<androidx.coordinatorlayout/>
<androidx.fragment/>
<androidx.tvprovider/>
<androidx.slice/>
<androidx.collection/>
<androidx.recommendation/>
<androidx.drawerlayout/>
<androidx.recyclerview/>
<androidx.webkit/>
<androidx.palette/>
<com.google.ar.sceneform/>
<com.google.ar.sceneform.ux/>
<androidx.test.ext/>
<com.google.android.ads.consent/>
<androidx.versionedparcelable/>
<androidx.media2/>
<com.google.ads.afsn/>
<com.google.android.ads/>
<androidx.biometric/>
<androidx.concurrent/>
<androidx.activity/>
<com.android.tools.apkparser/>
<com.android.tools.pixelprobe/>
<androidx.textclassifier/>
<androidx.remotecallback/>
<com.android.tools.chunkio/>
<com.android.tools.fakeadbserver/>
<androidx.savedstate/>
</metadata>

2
android/gradle/wrapper/gradle-wrapper.properties

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip

2
android/metadata/en-US/full_description.txt

@ -1,6 +1,6 @@
Store, send and receive bitcoin with the wallet focus on security and simplicity.
On Blue Wallet you own you private keys.
On BlueWallet you own you private keys.
You can instantly transact with anyone in the world and transform the financial system right from your pocket.

22
android/settings.gradle

@ -1,14 +1,16 @@
rootProject.name = 'BlueWallet'
include ':react-native-camera'
project(':react-native-camera').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-camera/android')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-tcp'
project(':react-native-tcp').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-tcp/android')
include ':@remobile_react-native-qrcode-local-image'
project(':@remobile_react-native-qrcode-local-image').projectDir = new File(rootProject.projectDir, '../node_modules/@remobile/react-native-qrcode-local-image/android')
include ':react-native-image-picker'
project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android')
include ':react-native-webview'
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-sentry'
project(':react-native-sentry').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sentry/android')
include ':react-native-randombytes'
@ -21,6 +23,10 @@ include ':react-native-haptic-feedback'
project(':react-native-haptic-feedback').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-haptic-feedback/android')
include ':react-native-google-analytics-bridge'
project(':react-native-google-analytics-bridge').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-analytics-bridge/android')
include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
include ':react-native-device-info'
project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-device-info/android')
include ':react-native-camera'

5
app.json

@ -1,7 +1,4 @@
{
"displayName": "Blue Wallet",
"name": "BlueWallet",
"ios": {
"buildNumber": "118"
}
"displayName": "BlueWallet"
}

3
babel.config.js

@ -0,0 +1,3 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
};

77
class/abstract-hd-wallet.js

@ -4,6 +4,7 @@ import { WatchOnlyWallet } from './watch-only-wallet';
const bip39 = require('bip39');
const BigNumber = require('bignumber.js');
const bitcoin = require('bitcoinjs-lib');
const BlueElectrum = require('../BlueElectrum');
export class AbstractHDWallet extends LegacyWallet {
static type = 'abstract';
@ -346,24 +347,68 @@ export class AbstractHDWallet extends LegacyWallet {
async fetchBalance() {
try {
const api = new Frisbee({ baseURI: 'https://www.blockonomics.co' });
let response = await api.post('/api/balance', { body: JSON.stringify({ addr: this.getXpub() }) });
if (response && response.body && response.body.response) {
this.balance = 0;
this.unconfirmed_balance = 0;
this.usedAddresses = [];
for (let addr of response.body.response) {
this.balance += addr.confirmed;
this.unconfirmed_balance += addr.unconfirmed;
this.usedAddresses.push(addr.addr);
// doing binary search for last used externa address
let that = this;
// refactor me
// eslint-disable-next-line
async function binarySearchIterationForInternalAddress(index, maxUsedIndex = 0, minUnusedIndex = 100500100, depth = 0) {
if (depth >= 20) return maxUsedIndex + 1; // fail
let txs = await BlueElectrum.getTransactionsByAddress(that._getInternalAddressByIndex(index));
if (txs.length === 0) {
if (index === 0) return 0;
minUnusedIndex = Math.min(minUnusedIndex, index); // set
index = Math.floor((index - maxUsedIndex) / 2 + maxUsedIndex);
} else {
maxUsedIndex = Math.max(maxUsedIndex, index); // set
let txs2 = await BlueElectrum.getTransactionsByAddress(that._getInternalAddressByIndex(index + 1));
if (txs2.length === 0) return index + 1; // thats our next free address
index = Math.round((minUnusedIndex - index) / 2 + index);
}
return binarySearchIterationForInternalAddress(index, maxUsedIndex, minUnusedIndex, depth + 1);
}
this.next_free_change_address_index = await binarySearchIterationForInternalAddress(100);
// refactor me
// eslint-disable-next-line
async function binarySearchIterationForExternalAddress(index, maxUsedIndex = 0, minUnusedIndex = 100500100, depth = 0) {
if (depth >= 20) return maxUsedIndex + 1; // fail
let txs = await BlueElectrum.getTransactionsByAddress(that._getExternalAddressByIndex(index));
if (txs.length === 0) {
if (index === 0) return 0;
minUnusedIndex = Math.min(minUnusedIndex, index); // set
index = Math.floor((index - maxUsedIndex) / 2 + maxUsedIndex);
} else {
maxUsedIndex = Math.max(maxUsedIndex, index); // set
let txs2 = await BlueElectrum.getTransactionsByAddress(that._getExternalAddressByIndex(index + 1));
if (txs2.length === 0) return index + 1; // thats our next free address
index = Math.round((minUnusedIndex - index) / 2 + index);
}
this.balance = new BigNumber(this.balance).dividedBy(100000000).toString() * 1;
this.unconfirmed_balance = new BigNumber(this.unconfirmed_balance).dividedBy(100000000).toString() * 1;
this._lastBalanceFetch = +new Date();
} else {
throw new Error('Could not fetch balance from API: ' + response.err);
return binarySearchIterationForExternalAddress(index, maxUsedIndex, minUnusedIndex, depth + 1);
}
this.next_free_address_index = await binarySearchIterationForExternalAddress(100);
this.usedAddresses = [];
// generating all involved addresses:
for (let c = 0; c < this.next_free_address_index; c++) {
this.usedAddresses.push(this._getExternalAddressByIndex(c));
}
for (let c = 0; c < this.next_free_change_address_index; c++) {
this.usedAddresses.push(this._getInternalAddressByIndex(c));
}
// finally fetching balance
let balance = await BlueElectrum.multiGetBalanceByAddress(this.usedAddresses);
this.balance = new BigNumber(balance.balance).dividedBy(100000000).toNumber();
this.unconfirmed_balance = new BigNumber(balance.unconfirmed_balance).dividedBy(100000000).toNumber();
this._lastBalanceFetch = +new Date();
} catch (err) {
console.warn(err);
}

15
class/app-storage.js

@ -262,6 +262,7 @@ export class AppStorage {
* @return {Promise.<void>}
*/
async fetchWalletBalances(index) {
console.log('fetchWalletBalances for wallet#', index);
if (index || index === 0) {
let c = 0;
for (let wallet of this.wallets) {
@ -287,6 +288,7 @@ export class AppStorage {
* @return {Promise.<void>}
*/
async fetchWalletTransactions(index) {
console.log('fetchWalletTransactions for wallet#', index);
if (index || index === 0) {
let c = 0;
for (let wallet of this.wallets) {
@ -325,10 +327,11 @@ export class AppStorage {
* Getter for all transactions in all wallets.
* But if index is provided - only for wallet with corresponding index
*
* @param index {Integer} Wallet index in this.wallets. Empty for all wallets.
* @param index {Integer|null} Wallet index in this.wallets. Empty (or null) for all wallets.
* @param limit {Integer} How many txs return, starting from the earliest. Default: all of them.
* @return {Array}
*/
getTransactions(index) {
getTransactions(index, limit = Infinity) {
if (index || index === 0) {
let txs = [];
let c = 0;
@ -353,9 +356,11 @@ export class AppStorage {
t.sort_ts = +new Date(t.received);
}
return txs.sort(function(a, b) {
return b.sort_ts - a.sort_ts;
});
return txs
.sort(function(a, b) {
return b.sort_ts - a.sort_ts;
})
.slice(0, limit);
}
/**

66
class/hd-legacy-breadwallet-wallet.js

@ -1,7 +1,7 @@
import { AbstractHDWallet } from './abstract-hd-wallet';
import Frisbee from 'frisbee';
const bitcoin = require('bitcoinjs-lib');
const bip39 = require('bip39');
import bitcoin from 'bitcoinjs-lib';
import bip39 from 'bip39';
/**
* HD Wallet (BIP39).
@ -20,25 +20,26 @@ export class HDLegacyBreadwalletWallet extends AbstractHDWallet {
if (this._xpub) {
return this._xpub; // cache hit
}
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/0'";
let child = root.derivePath(path).neutered();
const path = "m/0'";
const child = root.derivePath(path).neutered();
this._xpub = child.toBase58();
return this._xpub;
}
_getExternalAddressByIndex(index) {
index = index * 1; // cast to int
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/0'/0/" + index;
let child = root.derivePath(path);
const path = "m/0'/0/" + index;
const child = root.derivePath(path);
return (this.external_addresses_cache[index] = child.getAddress());
}
@ -46,33 +47,38 @@ export class HDLegacyBreadwalletWallet extends AbstractHDWallet {
_getInternalAddressByIndex(index) {
index = index * 1; // cast to int
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/0'/1/" + index;
let child = root.derivePath(path);
const path = "m/0'/1/" + index;
const child = root.derivePath(path);
return (this.internal_addresses_cache[index] = child.getAddress());
}
_getExternalWIFByIndex(index) {
index = index * 1; // cast to int
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/0'/0/" + index;
let child = root.derivePath(path);
return child.keyPair.toWIF();
return this._getWIFByIndex(false, index);
}
_getInternalWIFByIndex(index) {
index = index * 1; // cast to int
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/0'/1/" + index;
let child = root.derivePath(path);
return this._getWIFByIndex(true, index);
}
/**
* Get internal/external WIF by wallet index
* @param {Boolean} internal
* @param {Number} index
* @returns {*}
* @private
*/
_getWIFByIndex(internal, index) {
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.HDNode.fromSeedBuffer(seed);
const path = `m/0'/${internal ? 1 : 0}/${index}`;
const child = root.derivePath(path);
return child.keyPair.toWIF();
}

105
class/hd-legacy-p2pkh-wallet.js

@ -1,9 +1,8 @@
import { LegacyWallet } from './';
import { AbstractHDWallet } from './abstract-hd-wallet';
const bitcoin = require('bitcoinjs-lib');
const bip39 = require('bip39');
const BigNumber = require('bignumber.js');
const signer = require('../models/signer');
import bitcoin from 'bitcoinjs-lib';
import bip39 from 'bip39';
import BigNumber from 'bignumber.js';
import signer from '../models/signer';
/**
* HD Wallet (BIP39).
@ -22,82 +21,66 @@ export class HDLegacyP2PKHWallet extends AbstractHDWallet {
if (this._xpub) {
return this._xpub; // cache hit
}
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/44'/0'/0'";
let child = root.derivePath(path).neutered();
const path = "m/44'/0'/0'";
const child = root.derivePath(path).neutered();
this._xpub = child.toBase58();
return this._xpub;
}
_getExternalWIFByIndex(index) {
index = index * 1; // cast to int
if (index < 0) return '';
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/44'/0'/0'/0/" + index;
let child = root.derivePath(path);
return child.keyPair.toWIF();
return this._getWIFByIndex(false, index);
}
_getInternalWIFByIndex(index) {
index = index * 1; // cast to int
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/44'/0'/0'/1/" + index;
let child = root.derivePath(path);
return this._getWIFByIndex(true, index);
}
/**
* Get internal/external WIF by wallet index
* @param {Boolean} internal
* @param {Number} index
* @returns {*}
* @private
*/
_getWIFByIndex(internal, index) {
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.HDNode.fromSeedBuffer(seed);
const path = `m/44'/0'/0'/${internal ? 1 : 0}/${index}`;
const child = root.derivePath(path);
return child.keyPair.toWIF();
}
_getExternalAddressByIndex(index) {
index = index * 1; // cast to int
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
if (!this._xpub) {
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/44'/0'/0'/0/" + index;
let child = root.derivePath(path);
let w = new LegacyWallet();
w.setSecret(child.keyPair.toWIF());
return (this.external_addresses_cache[index] = w.getAddress());
} else {
let node = bitcoin.HDNode.fromBase58(this._xpub);
let address = node
.derive(0)
.derive(index)
.getAddress();
return (this.external_addresses_cache[index] = address);
}
const node = bitcoin.HDNode.fromBase58(this.getXpub());
const address = node
.derive(0)
.derive(index)
.getAddress();
return (this.external_addresses_cache[index] = address);
}
_getInternalAddressByIndex(index) {
index = index * 1; // cast to int
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
if (!this._xpub) {
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/44'/0'/0'/1/" + index;
let child = root.derivePath(path);
let w = new LegacyWallet();
w.setSecret(child.keyPair.toWIF());
return (this.internal_addresses_cache[index] = w.getAddress());
} else {
let node = bitcoin.HDNode.fromBase58(this._xpub);
let address = node
.derive(1)
.derive(index)
.getAddress();
return (this.internal_addresses_cache[index] = address);
}
const node = bitcoin.HDNode.fromBase58(this.getXpub());
const address = node
.derive(1)
.derive(index)
.getAddress();
return (this.internal_addresses_cache[index] = address);
}
createTx(utxos, amount, fee, address) {

170
class/hd-segwit-p2sh-wallet.js

@ -1,12 +1,41 @@
import { AbstractHDWallet } from './abstract-hd-wallet';
import Frisbee from 'frisbee';
import { NativeModules } from 'react-native';
import bitcoin from 'bitcoinjs-lib';
import bip39 from 'bip39';
import BigNumber from 'bignumber.js';
import b58 from 'bs58check';
import signer from '../models/signer';
const { RNRandomBytes } = NativeModules;
const bitcoin = require('bitcoinjs-lib');
const bip39 = require('bip39');
const BigNumber = require('bignumber.js');
const b58 = require('bs58check');
const signer = require('../models/signer');
/**
* Converts ypub to xpub
* @param {String} ypub - wallet ypub
* @returns {*}
*/
function ypubToXpub(ypub) {
let data = b58.decode(ypub);
data = data.slice(4);
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
return b58.encode(data);
}
/**
* Creates Segwit P2SH Bitcoin address
* @param hdNode
* @returns {String}
*/
function nodeToP2shSegwitAddress(hdNode) {
const pubkeyBuf = hdNode.keyPair.getPublicKeyBuffer();
const hash = bitcoin.crypto.hash160(pubkeyBuf);
const redeemScript = bitcoin.script.witnessPubKeyHash.output.encode(hash);
const hash2 = bitcoin.crypto.hash160(redeemScript);
const scriptPubkey = bitcoin.script.scriptHash.output.encode(hash2);
return bitcoin.address.fromOutputScript(scriptPubkey);
}
/**
* HD Wallet (BIP39).
@ -45,108 +74,50 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
}
_getExternalWIFByIndex(index) {
index = index * 1; // cast to int
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/49'/0'/0'/0/" + index;
let child = root.derivePath(path);
return child.keyPair.toWIF();
return this._getWIFByIndex(false, index);
}
_getInternalWIFByIndex(index) {
index = index * 1; // cast to int
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/49'/0'/0'/1/" + index;
let child = root.derivePath(path);
return this._getWIFByIndex(true, index);
}
/**
* Get internal/external WIF by wallet index
* @param {Boolean} internal
* @param {Number} index
* @returns {*}
* @private
*/
_getWIFByIndex(internal, index) {
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.HDNode.fromSeedBuffer(seed);
const path = `m/49'/0'/0'/${internal ? 1 : 0}/${index}`;
const child = root.derivePath(path);
return child.keyPair.toWIF();
}
_getExternalAddressByIndex(index) {
index = index * 1; // cast to int
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
if (!this._xpub) {
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/49'/0'/0'/0/" + index;
let child = root.derivePath(path);
let keyhash = bitcoin.crypto.hash160(child.getPublicKeyBuffer());
let scriptSig = bitcoin.script.witnessPubKeyHash.output.encode(keyhash);
let addressBytes = bitcoin.crypto.hash160(scriptSig);
let outputScript = bitcoin.script.scriptHash.output.encode(addressBytes);
let address = bitcoin.address.fromOutputScript(outputScript, bitcoin.networks.bitcoin);
this._address_to_wif_cache[address] = child.keyPair.toWIF();
return (this.external_addresses_cache[index] = address);
} else {
let b58 = require('bs58check');
// eslint-disable-next-line
function ypubToXpub(ypub) {
var data = b58.decode(ypub);
data = data.slice(4);
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
return b58.encode(data);
}
// eslint-disable-next-line
function nodeToP2shSegwitAddress(hdNode) {
let pubkeyBuf = hdNode.keyPair.getPublicKeyBuffer();
let hash = bitcoin.crypto.hash160(pubkeyBuf);
let redeemScript = bitcoin.script.witnessPubKeyHash.output.encode(hash);
let hash2 = bitcoin.crypto.hash160(redeemScript);
let scriptPubkey = bitcoin.script.scriptHash.output.encode(hash2);
return bitcoin.address.fromOutputScript(scriptPubkey);
}
let xpub = ypubToXpub(this._xpub);
let hdNode = bitcoin.HDNode.fromBase58(xpub);
let address = nodeToP2shSegwitAddress(hdNode.derive(0).derive(index));
return (this.external_addresses_cache[index] = address);
}
const xpub = ypubToXpub(this.getXpub());
const hdNode = bitcoin.HDNode.fromBase58(xpub);
const address = nodeToP2shSegwitAddress(hdNode.derive(0).derive(index));
return (this.external_addresses_cache[index] = address);
}
_getInternalAddressByIndex(index) {
index = index * 1; // cast to int
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
if (!this._xpub) {
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/49'/0'/0'/1/" + index;
let child = root.derivePath(path);
let keyhash = bitcoin.crypto.hash160(child.getPublicKeyBuffer());
let scriptSig = bitcoin.script.witnessPubKeyHash.output.encode(keyhash);
let addressBytes = bitcoin.crypto.hash160(scriptSig);
let outputScript = bitcoin.script.scriptHash.output.encode(addressBytes);
let address = bitcoin.address.fromOutputScript(outputScript, bitcoin.networks.bitcoin);
this._address_to_wif_cache[address] = child.keyPair.toWIF();
return (this.internal_addresses_cache[index] = address);
} else {
let b58 = require('bs58check');
// eslint-disable-next-line
function ypubToXpub(ypub) {
var data = b58.decode(ypub);
data = data.slice(4);
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
return b58.encode(data);
}
// eslint-disable-next-line
function nodeToP2shSegwitAddress(hdNode) {
let pubkeyBuf = hdNode.keyPair.getPublicKeyBuffer();
let hash = bitcoin.crypto.hash160(pubkeyBuf);
let redeemScript = bitcoin.script.witnessPubKeyHash.output.encode(hash);
let hash2 = bitcoin.crypto.hash160(redeemScript);
let scriptPubkey = bitcoin.script.scriptHash.output.encode(hash2);
return bitcoin.address.fromOutputScript(scriptPubkey);
}
let xpub = ypubToXpub(this._xpub);
let hdNode = bitcoin.HDNode.fromBase58(xpub);
let address = nodeToP2shSegwitAddress(hdNode.derive(1).derive(index));
return (this.internal_addresses_cache[index] = address);
}
const xpub = ypubToXpub(this.getXpub());
const hdNode = bitcoin.HDNode.fromBase58(xpub);
const address = nodeToP2shSegwitAddress(hdNode.derive(1).derive(index));
return (this.internal_addresses_cache[index] = address);
}
/**
@ -160,19 +131,20 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet {
return this._xpub; // cache hit
}
// first, getting xpub
let mnemonic = this.secret;
let seed = bip39.mnemonicToSeed(mnemonic);
let root = bitcoin.HDNode.fromSeedBuffer(seed);
const mnemonic = this.secret;
const seed = bip39.mnemonicToSeed(mnemonic);
const root = bitcoin.HDNode.fromSeedBuffer(seed);
let path = "m/49'/0'/0'";
let child = root.derivePath(path).neutered();
let xpub = child.toBase58();
const path = "m/49'/0'/0'";
const child = root.derivePath(path).neutered();
const xpub = child.toBase58();
// bitcoinjs does not support ypub yet, so we just convert it from xpub
let data = b58.decode(xpub);
data = data.slice(4);
data = Buffer.concat([Buffer.from('049d7cb2', 'hex'), data]);
this._xpub = b58.encode(data);
return this._xpub;
}

60
class/lightning-custodian-wallet.js

@ -7,8 +7,8 @@ export class LightningCustodianWallet extends LegacyWallet {
static type = 'lightningCustodianWallet';
static typeReadable = 'Lightning';
constructor() {
super();
constructor(props) {
super(props);
this.setBaseURI(); // no args to init with default value
this.init();
this.refresh_token = '';
@ -17,6 +17,7 @@ export class LightningCustodianWallet extends LegacyWallet {
this._access_token_created_ts = 0;
this.refill_addressess = [];
this.pending_transactions_raw = [];
this.user_invoices_raw = [];
this.info_raw = false;
this.preferredBalanceUnit = BitcoinUnit.SATS;
}
@ -39,8 +40,7 @@ export class LightningCustodianWallet extends LegacyWallet {
}
allowSend() {
console.log(this.getBalance(), this.getBalance() > 0);
return this.getBalance() > 0;
return true;
}
getAddress() {
@ -48,13 +48,11 @@ export class LightningCustodianWallet extends LegacyWallet {
}
timeToRefreshBalance() {
// lndhub calls are cheap, so why not refresh constantly
return true;
return (+new Date() - this._lastBalanceFetch) / 1000 > 3600; // 1hr
}
timeToRefreshTransaction() {
// lndhub calls are cheap, so why not refresh the list constantly
return true;
return (+new Date() - this._lastTxFetch) / 1000 > 3600; // 1hr
}
static fromJson(param) {
@ -111,6 +109,17 @@ export class LightningCustodianWallet extends LegacyWallet {
Authorization: 'Bearer' + ' ' + this.access_token,
},
});
if (response.originalResponse && typeof response.originalResponse === 'string') {
try {
response.originalResponse = JSON.parse(response.originalResponse);
} catch (_) {}
}
if (response.originalResponse && response.originalResponse.status && response.originalResponse.status === 503) {
throw new Error('Payment is in transit');
}
let json = response.body;
if (typeof json === 'undefined') {
throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.originalResponse));
@ -128,8 +137,10 @@ export class LightningCustodianWallet extends LegacyWallet {
*
* @return {Promise.<Array>}
*/
async getUserInvoices() {
let response = await this._api.get('/getuserinvoices', {
async getUserInvoices(limit = false) {
let limitString = '';
if (limit) limitString = '?limit=' + parseInt(limit);
let response = await this._api.get('/getuserinvoices' + limitString, {
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
@ -145,9 +156,30 @@ export class LightningCustodianWallet extends LegacyWallet {
throw new Error('API error: ' + json.message + ' (code ' + json.code + ')');
}
this.user_invoices_raw = json;
if (limit) {
// need to merge existing invoices with the ones that arrived
// but the ones received later should overwrite older ones
for (let oldInvoice of this.user_invoices_raw) {
// iterate all OLD invoices
let found = false;
for (let newInvoice of json) {
// iterate all NEW invoices
if (newInvoice.payment_request === oldInvoice.payment_request) found = true;
}
if (!found) {
// if old invoice is not found in NEW array, we simply add it:
json.push(oldInvoice);
}
}
}
this.user_invoices_raw = json.sort(function(a, b) {
return a.timestamp - b.timestamp;
});
return json;
return this.user_invoices_raw;
}
/**
@ -323,6 +355,7 @@ export class LightningCustodianWallet extends LegacyWallet {
txs = txs.concat(this.pending_transactions_raw.slice(), this.transactions_raw.slice().reverse(), this.user_invoices_raw.slice()); // slice so array is cloned
// transforming to how wallets/list screen expects it
for (let tx of txs) {
tx.fromWallet = this.secret;
if (tx.amount) {
// pending tx
tx.amt = tx.amount * -100000000;
@ -338,7 +371,7 @@ export class LightningCustodianWallet extends LegacyWallet {
if (tx.type === 'paid_invoice') {
tx.memo = tx.memo || 'Lightning payment';
if (tx.value > 0) tx.value = (tx.value * 1 + tx.fee * 1) * -1;
if (tx.value > 0) tx.value = tx.value * -1; // value already includes fee in it (see lndhub)
// outer code expects spending transactions to of negative value
}
@ -409,6 +442,7 @@ export class LightningCustodianWallet extends LegacyWallet {
throw new Error('API unexpected response: ' + JSON.stringify(response.body));
}
this._lastTxFetch = +new Date();
this.transactions_raw = json;
}

79
class/walletGradient.js

@ -0,0 +1,79 @@
import { LegacyWallet } from './legacy-wallet';
import { HDSegwitP2SHWallet } from './hd-segwit-p2sh-wallet';
import { LightningCustodianWallet } from './lightning-custodian-wallet';
import { HDLegacyBreadwalletWallet } from './hd-legacy-breadwallet-wallet';
import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet';
import { WatchOnlyWallet } from './watch-only-wallet';
export default class WalletGradient {
static defaultGradients = ['#65ceef', '#68bbe1'];
static watchOnlyWallet = ['#7d7d7d', '#4a4a4a'];
static legacyWallet = ['#40fad1', '#15be98'];
static hdLegacyP2PKHWallet = ['#e36dfa', '#bd10e0'];
static hdLegacyBreadWallet = ['#fe6381', '#f99c42'];
static hdSegwitP2SHWallet = ['#c65afb', '#9053fe'];
static lightningCustodianWallet = ['#f1be07', '#f79056'];
static createWallet = ['#eef0f4', '#eef0f4'];
static gradientsFor(type) {
let gradient;
switch (type) {
case WatchOnlyWallet.type:
gradient = WalletGradient.watchOnlyWallet;
break;
case LegacyWallet.type:
gradient = WalletGradient.legacyWallet;
break;
case HDLegacyP2PKHWallet.type:
gradient = WalletGradient.hdLegacyP2PKHWallet;
break;
case HDLegacyBreadwalletWallet.type:
gradient = WalletGradient.hdLegacyBreadWallet;
break;
case HDSegwitP2SHWallet.type:
gradient = WalletGradient.hdSegwitP2SHWallet;
break;
case LightningCustodianWallet.type:
gradient = WalletGradient.lightningCustodianWallet;
break;
case 'CreateWallet':
gradient = WalletGradient.createWallet;
break;
default:
gradient = WalletGradient.defaultGradients;
break;
}
return gradient;
}
static headerColorFor(type) {
let gradient;
switch (type) {
case WatchOnlyWallet.type:
gradient = WalletGradient.watchOnlyWallet;
break;
case LegacyWallet.type:
gradient = WalletGradient.legacyWallet;
break;
case HDLegacyP2PKHWallet.type:
gradient = WalletGradient.hdLegacyP2PKHWallet;
break;
case HDLegacyBreadwalletWallet.type:
gradient = WalletGradient.hdLegacyBreadWallet;
break;
case HDSegwitP2SHWallet.type:
gradient = WalletGradient.hdSegwitP2SHWallet;
break;
case LightningCustodianWallet.type:
gradient = WalletGradient.lightningCustodianWallet;
break;
case 'CreateWallet':
gradient = WalletGradient.createWallet;
break;
default:
gradient = WalletGradient.defaultGradients;
break;
}
return gradient[0];
}
}

4
events.js

@ -36,10 +36,6 @@ EV.enum = {
// changed (usually for current wallet)
REMOTE_TRANSACTIONS_COUNT_CHANGED: 'REMOTE_TRANSACTIONS_COUNT_CHANGED',
// emitted when QR scanner scanned address that should be used in CREATE TRANSACTION screen
// thus, previous screen (CREATE TRANSACTION screen) will update it's input content
CREATE_TRANSACTION_NEW_DESTINATION_ADDRESS: 'CREATE_TRANSACTION_NEW_DESTINATION_ADDRESS',
// RECEIVE_ADDRESS_CHANGED: 'RECEIVE_ADDRESS_CHANGED',
};

6
index.js

@ -24,13 +24,13 @@ class BlueAppComponent extends React.Component {
this.state = { isMigratingData: true };
}
async setIsMigratingData() {
setIsMigratingData = async () => {
await BlueApp.startAndDecrypt();
this.setState({ isMigratingData: false });
}
};
render() {
return this.state.isMigratingData ? <WalletMigrate onComplete={() => this.setIsMigratingData()} /> : <App />;
return this.state.isMigratingData ? <WalletMigrate onComplete={this.setIsMigratingData} /> : <App />;
}
}

1278
ios/BlueWallet.xcodeproj/project.pbxproj

File diff suppressed because it is too large

2
ios/BlueWallet/AppDelegate.h

@ -1,5 +1,5 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.

17
ios/BlueWallet/AppDelegate.m

@ -1,5 +1,5 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
@ -23,14 +23,12 @@
NSURL *jsCodeLocation;
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"BlueWallet"
initialProperties:nil
launchOptions:launchOptions];
[RNSentry installWithRootView:rootView];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
rootView.backgroundColor = [UIColor whiteColor];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
@ -40,11 +38,8 @@ RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
return YES;
}
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
return [RCTLinkingManager application:application openURL:url
sourceApplication:sourceApplication annotation:annotation];
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [RCTLinkingManager application:app openURL:url options:options];
}
@end

10
ios/BlueWallet/Info.plist

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Blue Wallet</string>
<string>BlueWallet</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>3.5.7</string>
<string>3.8.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -33,7 +33,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>224</string>
<string>232</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
@ -59,12 +59,14 @@
<string>In order to quickly scan the recipient's address, we need your permission to use the camera to scan their QR Code.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSMicrophoneUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSMotionUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<string>In order to import an image for scanning, we need your permission to access your photo library.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>UIAppFonts</key>

2
ios/BlueWallet/main.m

@ -1,5 +1,5 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.

2
ios/BlueWalletTests/BlueWalletTests.m

@ -1,5 +1,5 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.

2
ios/BlueWalletTests/Info.plist

@ -7,7 +7,7 @@
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>

2
ios/fastlane/metadata/en-US/description.txt

@ -1,6 +1,6 @@
Store, send and receive bitcoin with the wallet focus on security and simplicity.
On Blue Wallet you own you private keys. A Bitcoin wallet focused on us the users.
On BlueWallet you own you private keys. A Bitcoin wallet focused on us the users.
You can instantly transact with anyone in the world and transform the financial system right from your pocket.

56
ios/fastlane/metadata/en-US/release_notes.txt

@ -1,41 +1,17 @@
v.3.5.6
-------
v3.7.2
======
ADD: Loading indicator for wallet name change and wallet delete
ADD: Added CZK fiat
ADD: Alert user if they are trying to create a lightning wallet without a bitcoin wallet
ADD: French
ADD: DK - Danish Language Support
ADD: Thai locale
ADD: Dutch translation
FIX: Main wallet list shows transaction unit that is preferred by wallet
FIX: Export QRCode was too big on iPad
FIX: Don't override existing tx information when scanning invalid QR
FIX: Satoshis are now human-readable
FIX: crash when RBF
v3.5.5
------
ADD: pay zero-amount (tip) invoices
ADD: lightning withdrawal through zigzag
ADD: Thai translation
ADD: Dutch translation
ADD: Added Singapore Dollars
ADD: Added AUD, VEF, and ZAR fiats.
FIX: Loading indicator when creating a wallet
FIX: Changelly link
Fix and improve pt-BR translation
FIX: Cannot click on Lightning transactions #196
FIX: Fixed a clipping issue in lightning settings
FIX: fixed a margin issue in about that caused clipping
FIX: Changed invoice description field to label
FIX: Updated transaction buttons maximum width
FIX: Main Buttons layout #204
FIX: Add topup indication on wallet selection #207
FIX: Invoice QR code wrong scale #203
FIX: Don't allow user to pay for an invoice created with the same wallet.
FIX: If. balance was not a string, app would crash.
FIX: Changed language selection screen to FlatList
FIX: Made amount tap area larger
FIX: Fixed an issue in currency settings where the checkmark wouldn't be in the correct preference* Merge branch 'master' of github.com:BlueWallet/BlueWallet
* FIX: status bar disappears #311
* FIX: faulty back button on viewLndInvoice screen
* FIX: lnd - create invoice - cant tap bottom half of button #303
* FIX: Impossible to scan bech32 QR invoice with a specific amount (closes #296)
* REF: better lightning error reporting
* FIX: not redering QR code #302
* ADD: Indonesian Translation
* FIX: better LN wallet auto-refresh strategy
* FIX: Dismiss keyboard when pressing return
* FIX: HD wallet balance refresh
* FIX: no wallet refresh upon startup (faster to start app)
* FIX: Dismiss keyboard when pressing return
* REF: HD wallet getbalance improvements
* ADD: NZD as a currency

2
ios/fastlane/metadata/es-ES/description.txt

@ -1,6 +1,6 @@
Store, send and receive bitcoin with the wallet focus on security and simplicity.
On Blue Wallet you own you private keys. A Bitcoin wallet focused on us the users.
On BlueWallet you own you private keys. A Bitcoin wallet focused on us the users.
You can instantly transact with anyone in the world and transform the financial system right from your pocket.

2
ios/fastlane/metadata/pt-BR/description.txt

@ -1,6 +1,6 @@
Guardar, enviar e receber bitcoin com uma carteira focada na segurança e simplicidade.
Na Blue Wallet você possui as suas chaves privadas. Uma carteira Bitcoin focada nos usuários.
Na BlueWallet você possui as suas chaves privadas. Uma carteira Bitcoin focada nos usuários.
Você pode instantaneamente transacionar com qualquer pessoa no mundo e transformar o sistema financeiro diretamente do seu bolso.

2
ios/fastlane/metadata/pt-PT/description.txt

@ -1,6 +1,6 @@
Guardar, enviar e receber bitcoin com uma carteira focada na segurança e simplicidade.
Na Blue Wallet você possui as suas chaves privadas. Uma carteira Bitcoin focada nos usuários.
Na BlueWallet você possui as suas chaves privadas. Uma carteira Bitcoin focada nos usuários.
Você pode instantaneamente transacionar com qualquer pessoa no mundo e transformar o sistema financeiro diretamente do seu bolso.

14
loc/cs_CZ.js

@ -3,10 +3,6 @@ module.exports = {
storage_is_encrypted: 'Vaše úložiště je zašifrované. Zadejte heslo k odemčení',
enter_password: 'Zadejte heslo',
bad_password: 'Špatné heslo, prosím zkuste to znovu',
months_ago: 'měsíců',
days_ago: 'dní',
hours_ago: 'hodin',
minutes_ago: 'minut',
never: 'nikdy',
continue: 'Continue',
ok: 'OK',
@ -16,7 +12,7 @@ module.exports = {
options: 'možnosti',
createBitcoinWallet: 'In order to use a Lightning wallet, a Bitcoin wallet is needed to fund it. Would you like to continue anyway?',
list: {
app_name: 'Blue Wallet',
app_name: 'BlueWallet',
title: 'peněženky',
header: 'Peněženka reprezentuje pár tajného (privátního) klíče a adresy' + 'kterou můžete sdílet, abyste získali mince',
add: 'Přidat peněženku',
@ -163,6 +159,7 @@ module.exports = {
share: 'sdílet',
copiedToClipboard: 'Zkopírováno do schránky.',
label: 'Popis',
create: 'Create',
setAmount: 'Přijmout částku...',
},
},
@ -181,6 +178,12 @@ module.exports = {
retype_password: 'Heslo znovu',
passwords_do_not_match: 'Hesla se neshodují',
encrypt_storage: 'Zašifrovat úložiště',
lightning_settings: 'Lightning settings',
lightning_settings_explain:
'To connect to your own LND node please install LndHub' +
' and put its URL here in settings. Leave blank to use default ' +
'ndHub\n (lndhub.io)',
save: 'save',
about: 'O BlueWallet',
language: 'Jazyk',
currency: 'Měna',
@ -211,6 +214,7 @@ module.exports = {
refill: 'Doplnit',
withdraw: 'Vybrat',
expired: 'Expirováno',
placeholder: 'Invoice',
sameWalletAsInvoiceError: 'You can not pay an invoice with the same wallet used to create it.',
},
};

14
loc/da_DK.js

@ -3,10 +3,6 @@ module.exports = {
storage_is_encrypted: 'Lageret er krypteret. Indtast adgangskode for at dekryptere',
enter_password: 'Indtast adgangskode',
bad_password: 'Forkert adgangskode, prøv igen',
months_ago: 'måneder siden',
days_ago: 'dage siden',
hours_ago: 'timer siden',
minutes_ago: 'minutter siden',
never: 'aldrig',
continue: 'Continue',
ok: 'OK',
@ -16,7 +12,7 @@ module.exports = {
options: 'valgmuligheder',
createBitcoinWallet: 'In order to use a Lightning wallet, a Bitcoin wallet is needed to fund it. Would you like to continue anyway?',
list: {
app_name: 'Blue Wallet',
app_name: 'BlueWallet',
title: 'wallets',
header: 'En wallet består af par af hemmelige (private nøgler) og en adresse' + 'som du kan dele med andre for at modtage coins.',
add: 'Tilføj Wallet',
@ -163,6 +159,7 @@ module.exports = {
share: 'del',
copiedToClipboard: 'Kopieret til udklipsholder.',
label: 'Beskrivelse',
create: 'Create',
setAmount: 'Modtag med beløb',
},
},
@ -181,6 +178,12 @@ module.exports = {
retype_password: 'Gentag adgangskoden',
passwords_do_not_match: 'Adgangskoden er ikke den samme',
encrypt_storage: 'Krypter lager',
lightning_settings: 'Lightning settings',
lightning_settings_explain:
'To connect to your own LND node please install LndHub' +
' and put its URL here in settings. Leave blank to use default ' +
'ndHub\n (lndhub.io)',
save: 'save',
about: 'Andet',
language: 'Sprog',
currency: 'Valuta',
@ -207,6 +210,7 @@ module.exports = {
},
lnd: {
title: 'Administration',
placeholder: 'Invoice',
choose_source_wallet: 'Vælge en wallet',
refill_lnd_balance: 'Genopfyld Lightning wallet',
refill: 'Genopfyld',

65
loc/de_DE.js

@ -3,28 +3,24 @@ module.exports = {
storage_is_encrypted: 'Dein Speicher ist verschlüsselt. Zum Entschlüsseln wird ein Passwort benötigt.',
enter_password: 'Gib das Passwort ein',
bad_password: 'Fasches Passwort, nächster Versuch',
months_ago: 'Monate zurück',
days_ago: 'Tage zurück',
hours_ago: 'Stunden zurück',
minutes_ago: 'Minuten zurück',
never: 'nie',
continue: 'Continue',
continue: 'Weiter',
ok: 'OK',
},
wallets: {
select_wallet: 'Wähle Wallet',
select_wallet: 'Wähle eine Wallet',
options: 'Einstellungen',
createBitcoinWallet:
'In order to use a Lightning wallet, a Bitcoin wallet is needed in order to fund it. Please, create or import a Bitcoin wallet.',
'Um eine Lightning wallet zu verwenden, muss erstmal eine Bitcoin Wallet eingerichtet werden. Bitte erstell oder importier eine Bitcoin Wallet.',
list: {
app_name: 'Blue Wallet',
app_name: 'BlueWallet',
title: 'Wallets',
header:
'Eine Wallet (Brieftasche) spiegelt ein Paar von kryptographischen Schlüssel wider. Einen geheimen und eine Adresse als öffentlichen Schlüssel. Letztern kann man zum Erhalt von Bitcoin teilen.',
'Eine Wallet spiegelt ein Paar kryptographische Schlüssel wider. Einen geheimen Schlüseel und eine Adresse als öffentlichen Schlüssel. Den öffentlichen Schlüssel kann man zum Empfang von Bitcoin teilen.',
add: 'Wallet hinzufügen',
create_a_wallet: 'Wallet erstellen',
create_a_wallet1: 'Es ist kostenlos und du kannst',
create_a_wallet2: 'so viele erstellen, wie du möchtest',
create_a_wallet2: 'so viele Wallets erstellen, wie du möchtest',
latest_transaction: 'Lezte Transaktion',
empty_txs1: 'Deine Transaktionen erscheinen hier',
empty_txs2: 'Noch keine Transaktionen',
@ -46,7 +42,7 @@ module.exports = {
or: 'oder',
import_wallet: 'Wallet importieren',
imported: 'Importiert',
coming_soon: 'Folgt bald',
coming_soon: 'Demnächst verfügbar',
lightning: 'Lightning',
bitcoin: 'Bitcoin',
},
@ -81,15 +77,15 @@ module.exports = {
imported: 'Importiert',
error: 'Fehler beim Import. Ist die Eingabe korrekt?',
success: 'Erfolg',
do_import: 'Importiere',
do_import: 'Importieren',
scan_qr: 'oder QR-Code scannen?',
},
scanQrWif: {
go_back: 'Zurück',
cancel: 'Abbrechen',
decoding: 'Decodieren',
decoding: 'Entschlüsseln',
input_password: 'Passwort eingeben',
password_explain: 'Das ist ein BIP38 verschlüsselter geheimer Schlüssel',
password_explain: 'Das ist ein mit BIP38 verschlüsselter geheimer Schlüssel',
bad_password: 'Falsches Passwort',
wallet_already_exists: 'Diese Wallet existiert bereits',
bad_wif: 'Falsches WIF',
@ -120,11 +116,11 @@ module.exports = {
header: 'Senden',
details: {
title: 'Transaktion erstellen',
amount_field_is_not_valid: 'Betrageingabe ist nicht valide',
fee_field_is_not_valid: 'Gebühreingabe ist nicht valide',
address_field_is_not_valid: 'Adresseingabe ist nicht valide',
amount_field_is_not_valid: 'Betrageingabe ist nicht korrekt',
fee_field_is_not_valid: 'Gebühreingabe ist nicht korrekt',
address_field_is_not_valid: 'Adresseingabe ist nicht korrekt',
total_exceeds_balance: 'Der zu sendende Betrag ist größer als der verfügbare Betrag.',
create_tx_error: 'Fehler beim Erstellen der Transaktion. Bitte stelle sicher, dass die Adresse valide ist.',
create_tx_error: 'Fehler beim Erstellen der Transaktion. Bitte stelle sicher, dass die Adresse korrekt ist.',
address: 'Adresse',
amount_placeholder: 'Betrag (in BTC)',
fee_placeholder: 'plus Gebühr (in BTC)',
@ -165,6 +161,7 @@ module.exports = {
share: 'Teilen',
copiedToClipboard: 'In die Zwischenablage kopiert.',
label: 'Beschreibung',
create: 'Create',
setAmount: 'Zu erhaltender Betrag',
},
},
@ -175,30 +172,36 @@ module.exports = {
},
settings: {
header: 'Einstellungen',
plausible_deniability: 'Glaubhafte Abstreitbarkeit...',
storage_not_encrypted: 'Speicher: nicht verschlüsselt',
storage_encrypted: 'Speicher: verschlüsselt',
plausible_deniability: 'Glaubhafte Täuschung...',
storage_not_encrypted: 'Speicher nicht verschlüsselt',
storage_encrypted: 'Speicher verschlüsselt',
password: 'Passwort',
password_explain: 'Erstelle das Passwort zum Entschlüsseln des Speichers',
retype_password: 'Passwort wiederholen',
passwords_do_not_match: 'Passwörter stimmen nicht überein',
encrypt_storage: 'Speicher verschlüsseln',
lightning_settings: 'Lightning Einstellungen',
lightning_settings_explain:
'Bitte installier Lndhub, um mit deiner eigenen LND Node zu verbinden' +
' und setz seine URL hier in den Einstellungen. Lass das Feld leer, um Standard- ' +
'LndHub\n (lndhub.io) zu verwenden',
save: 'Speichern',
about: 'Über',
language: 'Sprache',
currency: 'Währung',
},
plausibledeniability: {
title: 'Glaubhafte Abstreitbarkeit',
title: 'Glaubhafte Täuschung',
help:
'Unter bestimmten Umständen könntest du dazu gezwungen werden, ' +
'dein Passwort preiszugeben. Um deine Bitcoins zu sichern, kann ' +
'BlueWallet einen weiteren verschlüsselten Speicher mit einem ' +
'anderen Passwort erstellen. Unter äußeren Druck kannst du das ' +
'anderen Passwort erstellen. Unter Druck kannst du das ' +
'zweite Passwort an Fremde weitergeben. Wenn eingegeben, öffnet ' +
'BlueWallet einen anderen Speicher zur Täuschung. Dies wirkt ' +
'auf Fremde täuschen echt und dein Hauptspeicher bleibt geheim ' +
'auf Fremde täuschend echt und dein Hauptspeicher bleibt geheim ' +
'und sicher.',
help2: 'Der weitere Speicher ist voll funktional und man kann einen Minimalbetrag für die Glaubhaftigkeit hinterlegen.',
help2: 'Der andere Speicher ist voll funktional und man kann einen Minimalbetrag für die Glaubhaftigkeit hinterlegen.',
create_fake_storage: 'Erstelle verschlüsselten Speicher zur Täuschung',
go_back: 'Zurück',
create_password: 'Erstelle ein Passwort',
@ -206,14 +209,16 @@ module.exports = {
password_should_not_match: 'Das Passwort für den täuschenden Speicher darf nicht mit dem deines Hauptspeichers übereinstimmen',
retype_password: 'Passwort wiederholen',
passwords_do_not_match: 'Passwörter stimmen nicht überein. Neuer Versuch',
success: 'Erfolg',
success: 'Erfolg!',
},
lnd: {
title: 'Beträge verwalten',
choose_source_wallet: 'Wähle eine Wallet als Quelle',
refill_lnd_balance: 'Fülle deine Lightning Wallet auf',
refill: 'Auffüllen',
choose_source_wallet: 'Wähle eine Wallet als Zahlungsquelle',
refill_lnd_balance: 'Lade deine Lightning Wallet auf',
refill: 'Aufladen',
withdraw: 'Abheben',
sameWalletAsInvoiceError: 'You can not pay an invoice with the same wallet used to create it.',
placeholder: 'Invoice',
sameWalletAsInvoiceError:
'Du kannst nicht die Rechnung mit der Wallet begleichen, die du für die Erstellung dieser Rechnung verwendet hast.',
},
};

16
loc/en.js

@ -3,10 +3,6 @@ module.exports = {
storage_is_encrypted: 'Your storage is encrypted. Password is required to decrypt it',
enter_password: 'Enter password',
bad_password: 'Bad pasword, try again',
months_ago: 'months ago',
days_ago: 'days ago',
hours_ago: 'hours ago',
minutes_ago: 'minutes ago',
never: 'never',
continue: 'Continue',
ok: 'OK',
@ -14,9 +10,10 @@ module.exports = {
wallets: {
select_wallet: 'Select Wallet',
options: 'options',
createBitcoinWallet: 'In order to use a Lightning wallet, a Bitcoin wallet is needed to fund it. Would you like to continue anyway?',
createBitcoinWallet:
'You currently do not have a Bitcoin wallet. In order to fund a Lightning wallet, a Bitcoin wallet needs to be created or imported. Would you like to continue anyway?',
list: {
app_name: 'Blue Wallet',
app_name: 'BlueWallet',
title: 'wallets',
header: 'A wallet represents a pair of a secret (private key) and an address' + 'you can share to receive coins.',
add: 'Add Wallet',
@ -163,6 +160,7 @@ module.exports = {
share: 'share',
copiedToClipboard: 'Copied to clipboard.',
label: 'Description',
create: 'Create',
setAmount: 'Receive with amount',
},
},
@ -181,6 +179,11 @@ module.exports = {
retype_password: 'Re-type password',
passwords_do_not_match: 'Passwords do not match',
encrypt_storage: 'Encrypt storage',
lightning_settings: 'Lightning Settings',
lightning_settings_explain:
'To connect to your own LND node please install LndHub' +
' and put its URL here in settings. Leave blank to use default LndHub (lndhub.io)',
save: 'save',
about: 'About',
language: 'Language',
currency: 'Currency',
@ -212,6 +215,7 @@ module.exports = {
refill: 'Refill',
withdraw: 'Withdraw',
expired: 'Expired',
placeholder: 'Invoice',
sameWalletAsInvoiceError: 'You can not pay an invoice with the same wallet used to create it.',
},
};

14
loc/es.js

@ -3,10 +3,6 @@ module.exports = {
storage_is_encrypted: 'Su almacenamiento está cifrado. Se requiere contraseña para descifrarla.',
enter_password: 'Inserte contraseña',
bad_password: 'Contraseña incorrecta. Intente nuevamente.',
months_ago: 'meses atras',
days_ago: 'dias atras',
hours_ago: 'horas atras',
minutes_ago: 'minutos atras',
never: 'nunca',
continue: 'Continua',
ok: 'OK',
@ -17,7 +13,7 @@ module.exports = {
createBitcoinWallet:
'In order to use a Lightning wallet, a Bitcoin wallet is needed in order to fund it. Would you like to continue anyway?',
list: {
app_name: 'Blue Wallet',
app_name: 'BlueWallet',
title: 'billeteras',
header: 'Un Monedero esta representado con secreto (clave privada) y una dirección' + 'que puedes compartir para recibir monedas.',
add: 'Añadir Carterqa',
@ -165,6 +161,7 @@ module.exports = {
share: 'Compartir',
copiedToClipboard: 'Copiado a portapapeles.',
label: 'Description',
create: 'Create',
setAmount: 'Receive with amount',
},
},
@ -184,6 +181,12 @@ module.exports = {
retype_password: 'Ingresa la contraseña nuevamente',
passwords_do_not_match: 'Contraseñas deben ser iguales',
encrypt_storage: 'Cifrar almacenamiento',
lightning_settings: 'Lightning settings',
lightning_settings_explain:
'To connect to your own LND node please install LndHub' +
' and put its URL here in settings. Leave blank to use default ' +
'ndHub\n (lndhub.io)',
save: 'save',
about: 'Sobre nosotros',
language: 'Idioma',
currency: 'Moneda',
@ -214,6 +217,7 @@ module.exports = {
refill_lnd_balance: 'Rellenar el balance de la billetera Lightning',
refill: 'Rellenar',
withdraw: 'Retirar',
placeholder: 'Invoice',
expired: 'Expirado',
sameWalletAsInvoiceError: 'You can not pay an invoice with the same wallet used to create it.',
},

14
loc/fr_FR.js

@ -3,10 +3,6 @@ module.exports = {
storage_is_encrypted: "L'espace de stockage est chiffré. Mot de passe requis pour le déchiffrer.",
enter_password: 'Saisir mot de passe',
bad_password: 'Mauvais mot de passe, ré-essayer',
months_ago: 'mois',
days_ago: 'jours',
hours_ago: 'heures',
minutes_ago: 'minutes',
never: 'jamais',
continue: 'Continue',
ok: 'OK',
@ -17,7 +13,7 @@ module.exports = {
createBitcoinWallet: 'In order to use a Lightning wallet, a Bitcoin wallet is needed to fund it. Would you like to continue anyway?',
list: {
app_name: 'Blue Wallet',
app_name: 'BlueWallet',
title: 'portefeuilles',
header:
'Un portefeuille represente une paire de clées (publique/privée) et une adresse que vous pouvez partager pour recevoir des transactions.',
@ -165,6 +161,7 @@ module.exports = {
share: 'partager',
copiedToClipboard: 'Copier dans le presse-papiers.',
label: 'Description',
create: 'Create',
setAmount: 'Revevoir avec montant',
},
},
@ -183,6 +180,12 @@ module.exports = {
retype_password: 'Re-saisir votre mot de passe',
passwords_do_not_match: 'Les mots de passe ne correspondent pas',
encrypt_storage: 'Chiffrer le stockage',
lightning_settings: 'Lightning settings',
lightning_settings_explain:
'To connect to your own LND node please install LndHub' +
' and put its URL here in settings. Leave blank to use default ' +
'ndHub\n (lndhub.io)',
save: 'save',
about: 'À propos',
language: 'Langue',
currency: 'Devise',
@ -213,6 +216,7 @@ module.exports = {
refill_lnd_balance: 'Déposer des fonds dans votre portfeuille Lightning',
refill: 'Déposer des fonds',
withdraw: 'Retirer des fonds',
placeholder: 'Invoice',
expired: 'Expiré',
sameWalletAsInvoiceError: 'Vous ne pouvez pas payer une facture avec le même portefeuille utilisé pour la créer.',
},

54
loc/hr_HR.js

@ -3,19 +3,17 @@ module.exports = {
storage_is_encrypted: 'Vaš spremnik je kriptiran. Za dekripcoju je potrebna lozinka.',
enter_password: 'Unesi lozinku',
bad_password: 'Kriva lozinka, pokušaj ponovo',
months_ago: 'mjesecima unazad',
days_ago: 'danima unazad',
hours_ago: 'satima unazad',
minutes_ago: 'minutama unazad',
never: 'nikad',
continue: 'Nastavi',
ok: 'U redu',
},
wallets: {
select_wallet: 'Odaberi volet',
options: 'opcije',
list: {
app_name: 'Blue Wallet',
app_name: 'BlueWallet',
title: 'Voleti',
header: 'Volet je par privatnog ključa (tajna!) i javne adrese' + 'koju slobodno možete dijeliti kada primate novce.',
header: 'Volet je par privatnog ključa (tajna!) i javne adrese ' + 'koju slobodno možete dijeliti kada primate novce.',
add: 'Dodaj volet',
create_a_wallet: 'Stvori novi volet',
create_a_wallet1: 'Ne košta ništa i možete',
@ -35,10 +33,10 @@ module.exports = {
create: 'Stvori',
label_new_segwit: 'Novi SegWit',
label_new_lightning: 'Novi Lightning',
wallet_name: 'ime voleta',
wallet_type: 'tip',
wallet_name: 'ime voleta:',
wallet_type: 'tip:',
or: 'ili',
import_wallet: 'Unesi volet',
import_wallet: 'Unesi vanjski volet',
imported: 'Unešeno',
coming_soon: 'Dolazi uskoro',
lightning: 'Lightning',
@ -55,10 +53,10 @@ module.exports = {
yes_delete: 'Da, briši',
no_cancel: 'Ne, otiaži',
delete: 'Obriši',
save: 'Pohrani',
save: 'Spremi',
delete_this_wallet: 'Obriši ovaj volet',
export_backup: 'Izvoz / bekap',
buy_bitcoin: 'Kupi Bitcoin',
buy_bitcoin: 'Kupovina Bitcoina',
show_xpub: 'Prikaži voletov XPUB',
},
export: {
@ -149,21 +147,22 @@ module.exports = {
satoshi_per_byte: 'Satoshi / byte',
memo: 'Bilješka',
broadcast: 'Objavi',
not_enough_fee: 'Trošak slanja je premal. Povećaj ga.',
not_enough_fee: 'Trošak slanja je premalen. Povećaj ga.',
},
},
receive: {
header: 'Primanje',
header: 'Primi',
details: {
title: 'Pokaži ovu adresu platitelju',
share: 'pokaži',
share: 'podijeli',
copiedToClipboard: 'Kopirano u međuspremnik.',
label: 'Opis',
create: 'Stvori',
setAmount: 'Odredi iznos za primiti',
},
},
buyBitcoin: {
header: 'Kupi Bitcoin',
header: 'Kupovina Bitcoina',
tap_your_address: 'Klikni na adresu za spremanje u međuspremnik:',
copied: 'Spremljeno u međuspremnik!',
},
@ -177,20 +176,26 @@ module.exports = {
retype_password: 'Ponovi lozinku',
passwords_do_not_match: 'Lozinke su različite',
encrypt_storage: 'Kriptiraj spremnik',
about: 'Iznos',
lightning_settings: 'Lightning postavke',
lightning_settings_explain:
'Za spajanje na tvoj vlastiti LND čvor trebaš instalirati LndHub' +
' i upisati njegov URL ovdje. Ostavi prazno za standardni ' +
'ndHub\n (lndhub.io)',
save: 'Spremi',
about: 'Informacije',
language: 'Jezik',
currency: 'Valuta',
},
plausibledeniability: {
title: 'Fejk volet',
help:
'U iznimnim okolnostima netko gadan (pa još ako drži oklagiju) te' +
'može neljubazno pritisnuti da mu otkriješ lozinku za ovaj volet.' +
'BlueWallet ti čuva leđa buraz. Nemaš brige. Gledaj.' +
'Stvoriti ćemo dupli volet sa drugačijom lozinkom. Haha, žišku?' +
'Pa kad se ovaj počne pjeniti a ti vidiš da je vrag odnio šalu' +
'ti mu podvali ovaj drugi volet. Eto mu ga. Nek si cucla.',
help2: 'Novi spremnik će biti posve funkcionalan, možeš pohraniti koliko' + 'misliš da je potrebno da izgleda uvjerljivo.',
'Pazi. Netko gadan te može u iznimnim okolnostima (pljačka, prijevremeni izbori, itd.) ' +
'brutalno pritisnuti da mu otkriješ lozinku za svoj volet. ' +
'BlueWallet ti čuva leđa buraz. Nemaš brige. Gledaj, ' +
'stvoriti ćemo fejk volet sa drugačijom lozinkom. Haha, žišku? ' +
'Pa kad se ovaj počne pjeniti, a ti vidiš da je vrag odnio šalu, ' +
'samo mu podvali lozinku za ovaj drugi volet. Eto mu ga. Nek si cucla. ',
help2: 'Novi spremnik će biti posve funkcionalan, možeš pohraniti koliko ' + 'misliš da je potrebno da izgleda uvjerljivo.',
create_fake_storage: 'Stvori fejk enkriptirani spremnik',
go_back: 'Povratak',
create_password: 'Unesi lozinku',
@ -206,7 +211,8 @@ module.exports = {
refill_lnd_balance: 'Dopuni Lightning volet saldo',
refill: 'Dopuni',
withdraw: 'Isprazni',
placeholder: 'Invoice',
expired: 'Isteklo',
sameWalletAsInvoiceError: 'Ne možeš platiti račun s istim voletom s kojim si račun stvorio, ono.',
sameWalletAsInvoiceError: 'Buraz! Ne možeš platiti račun s istim voletom s kojim si račun stvorio, ono.',
},
};

220
loc/id_ID.js

@ -0,0 +1,220 @@
module.exports = {
_: {
storage_is_encrypted: 'Penyimpanan dienkripsi. Masukkan kata sandi untuk dekripsi:',
enter_password: 'Masukkan kata sandi',
bad_password: 'kata sandi salah, coba lagi',
never: 'tidak pernah',
continue: 'Lanjutkan',
ok: 'OK',
},
wallets: {
select_wallet: 'Pilih dompet',
options: 'Opsi',
createBitcoinWallet:
'Belum ada dompet bitcoin. Untuk mendanai dompet Lightning, dompet Bitcoin harus dibuat atau diimpor. Yakin ingin melanjutkan?',
list: {
app_name: 'BlueWallet',
title: 'Dompet',
header: 'Sebuah dompet mewakili sepasang kunci rahasia dan sebuah alamat' + 'yang bisa dipilih untuk menerima koin.',
add: 'Tambah dompet',
create_a_wallet: 'Buat dompet',
create_a_wallet1: 'Gratis dan bisa buat',
create_a_wallet2: 'sebanyak yang kamu mau',
latest_transaction: 'transaksi terbaru',
empty_txs1: 'Transaksimu akan muncul di sini,',
empty_txs2: 'saat ini tidak ada transaksi',
tap_here_to_buy: 'Tap di sini untuk membeli bitcoin',
},
reorder: {
title: 'Susun Dompet',
},
add: {
title: 'tambah dompet',
description:
'Kamu bisa membuat dompet atau memindai paper wallet dalam WIF (Wallet Import Format). Bluewallet mendukung dompet Segwit.',
scan: 'Pindai',
create: 'Buat',
label_new_segwit: 'Dompet SegWit baru',
label_new_lightning: 'Dompet Lightning baru',
wallet_name: 'nama dompet',
wallet_type: 'tipe',
or: 'atau',
import_wallet: 'Impor dompet',
imported: 'Diimpor',
coming_soon: 'Akan datang',
lightning: 'Lightning',
bitcoin: 'Bitcoin',
},
details: {
title: 'Dompet',
address: 'Alamat',
type: 'Tipe',
label: 'Label',
destination: 'tujuan',
description: 'deskripsi',
are_you_sure: 'Yakin?',
yes_delete: 'Ya, hapus',
no_cancel: 'Tidak, batalkan',
delete: 'Hapus',
save: 'Simpan',
delete_this_wallet: 'Hapus dompet ini',
export_backup: 'Ekspor / backup',
buy_bitcoin: 'Beli Bitcoin',
show_xpub: 'Tampilkan XPUB dompet',
},
export: {
title: 'ekspor dompet',
},
xpub: {
title: 'XPUB dompet',
copiedToClipboard: 'Disalin ke clipboard.',
},
import: {
title: 'impor',
explanation: 'Ketik kata mnemonic, private key, WIF, atau apapun yang kamu punya. BlueWallet akan mencoba mengimpor dompet kamu.',
imported: 'Diimpor',
error: 'Gagal mengimpor. Pastikan data yang diketik benar.',
success: 'Berhasil',
do_import: 'Impor',
scan_qr: 'atau mau pindai QR code?',
},
scanQrWif: {
go_back: 'Kembali',
cancel: 'Batal',
decoding: 'Membaca...',
input_password: 'Masukkan kata sandi',
password_explain: 'Ini adalah private key terenkripsi BIP38',
bad_password: 'kata sandi salah',
wallet_already_exists: 'Dompet sudah ada',
bad_wif: 'WIF salah',
imported_wif: 'WIF diimpor ',
with_address: ' dengan alamat ',
imported_segwit: 'Dompet SegWit diimpor',
imported_legacy: 'Dompet lawas diimpor',
imported_watchonly: 'Alamat tinjauan diimpor',
},
},
transactions: {
list: {
tabBarLabel: 'Transaksi',
title: 'transaksi',
description: 'Daftar transaksi keluar dan masuk dompet',
conf: 'konfirmasi',
},
details: {
title: 'Transaksi',
from: 'Input',
to: 'Output',
copy: 'Salin',
transaction_details: 'Detail transaksi',
show_in_block_explorer: 'Tampilkan di block explorer',
},
},
send: {
header: 'Kirim',
details: {
title: 'buat transaksi',
amount_field_is_not_valid: 'Jumlah tidak valid',
fee_field_is_not_valid: 'Tarif tidak valid',
address_field_is_not_valid: 'Alamat tidak valid',
total_exceeds_balance: 'Jumlah yang dikirim melebihi saldo.',
create_tx_error: 'Kesalahan dalam membuat transaksi. Cek kembali alamat tujuan.',
address: 'alamat',
amount_placeholder: 'jumlah (dalam BTC)',
fee_placeholder: 'Tambahan biaya transaksi (dalam BTC)',
note_placeholder: 'catatan pribadi',
cancel: 'Batalkan',
scan: 'Pindai',
send: 'Kirim',
create: 'Buat',
remaining_balance: 'Sisa saldo',
},
confirm: {
header: 'Konfirmasi',
sendNow: 'Kirim sekarang',
},
success: {
done: 'Selesai',
},
create: {
details: 'Detail',
title: 'buat transaksi',
error: 'Tidak bisa membuat transaksi. Cek alamat atau jumlah transfer.',
go_back: 'Kembali',
this_is_hex: 'Ini adalah hex transaksi, siap untuk disiarkan ke jaringan.',
to: 'Ke',
amount: 'Jumlah',
fee: 'Tarif',
tx_size: 'Ukuran TX',
satoshi_per_byte: 'Satoshi per byte',
memo: 'Memo',
broadcast: 'Siarkan',
not_enough_fee: 'Tarif tidak cukup. Naikkan tarif',
},
},
receive: {
header: 'Terima',
details: {
title: 'Bagikan alamat ini ke pengirim',
share: 'bagikan',
copiedToClipboard: 'Disalin ke clipboard.',
label: 'Deskripsi',
create: 'Buat',
setAmount: 'Terima sejumlah',
},
},
buyBitcoin: {
header: 'Beli bitcoin',
tap_your_address: 'Untuk menyalin, tap alamat:',
copied: 'Disalin ke Clipboard!',
},
settings: {
header: 'setting',
plausible_deniability: 'Plausible deniability...',
storage_not_encrypted: 'Penyimpanan: tidak terenkripsi',
storage_encrypted: 'Penyimpanan: terenkripsi',
password: 'kata sandi',
password_explain: 'Buat kata sandi untuk dekripsi penyimpanan',
retype_password: 'Ulangi kata sandi',
passwords_do_not_match: 'Kata sandi tidak cocok',
encrypt_storage: 'Enkripsi penyimpanan',
lightning_settings: 'Pengaturan Lightning',
lightning_settings_explain:
'Pasang LndHub untuk menghubungkan ke node LND kamu' +
' dan masukkan URL di sini. Biarkan kosong untuk menghubungkan ke LndHub standar (lndhub.io)',
save: 'simpan',
about: 'Tentang',
language: 'Bahasa',
currency: 'Mata Uang',
},
plausibledeniability: {
title: 'Plausible Deniability',
help:
'Under certain circumstances, you might be forced to disclose a ' +
'password. To keep your coins safe, BlueWallet can create another ' +
'encrypted storage, with a different password. Under pressure, ' +
'you can disclose this password to a 3rd party. If entered in ' +
"BlueWallet, it will unlock new 'fake' storage. This will seem " +
'legit to a 3rd party, but will secretly keep your main storage ' +
'with coins safe.',
help2: 'New storage will be fully functional, and you can store some ' + 'minimum amounts there so it looks more believable.',
create_fake_storage: 'Create fake encrypted storage',
go_back: 'Go Back',
create_password: 'Create a password',
create_password_explanation: 'Password for fake storage should not match password for your main storage',
password_should_not_match: 'Password for fake storage should not match password for your main storage',
retype_password: 'Retype password',
passwords_do_not_match: 'Passwords do not match, try again',
success: 'Success',
},
lnd: {
title: 'atur dana',
choose_source_wallet: 'Pilih dompet sumber',
refill_lnd_balance: 'Isi ulang saldo Lightning',
refill: 'Isi ulang',
withdraw: 'Tarik',
placeholder: 'Invoice',
expired: 'Kadaluarsa',
sameWalletAsInvoiceError: 'Kamu tidak bisa membayar invoice dengan dompet yang sama yang dipakai untuk membuat invoice.',
},
};

135
loc/index.js

@ -2,9 +2,12 @@ import Localization from 'react-localization';
import { AsyncStorage } from 'react-native';
import { AppStorage } from '../class';
import { BitcoinUnit } from '../models/bitcoinUnits';
import relativeTime from 'dayjs/plugin/relativeTime';
const dayjs = require('dayjs');
const currency = require('../currency');
const BigNumber = require('bignumber.js');
let strings;
dayjs.extend(relativeTime);
// first-time loading sequence
(async () => {
@ -12,14 +15,63 @@ let strings;
let lang = await AsyncStorage.getItem(AppStorage.LANG);
if (lang) {
strings.setLanguage(lang);
let localeForDayJSAvailable = true;
switch (lang) {
case 'zh':
require('dayjs/locale/zh-cn');
break;
case 'ru':
require('dayjs/locale/ru');
break;
case 'es':
require('dayjs/locale/es');
break;
case 'fr_fr':
require('dayjs/locale/fr');
break;
case 'pt_br':
lang = 'pt-br';
require('dayjs/locale/pt-br');
break;
case 'pt_pt':
lang = 'pt';
require('dayjs/locale/pt');
break;
case 'jp_jp':
lang = 'ja';
require('dayjs/locale/ja');
break;
case 'de_de':
require('dayjs/locale/de');
break;
case 'th_th':
require('dayjs/locale/th');
break;
case 'da_dk':
require('dayjs/locale/da');
break;
case 'nl_nl':
require('dayjs/locale/nl');
break;
case 'hr_hr':
require('dayjs/locale/hr');
break;
case 'id_id':
require('dayjs/locale/id');
break;
default:
localeForDayJSAvailable = false;
break;
}
if (localeForDayJSAvailable) {
dayjs.locale(lang.split('_')[0]);
}
return;
}
if (Localization.getCurrentLocaleAsync) {
let locale = await Localization.getCurrentLocaleAsync();
if (locale) {
locale = locale.split('-');
locale = locale[0];
console.log('current locale:', locale);
if (
locale === 'en' ||
@ -29,13 +81,60 @@ let strings;
locale === 'fr-fr' ||
locale === 'pt-br' ||
locale === 'pt-pt' ||
locale === 'jp-JP' ||
locale === 'de-de' ||
locale === 'cs-cz' ||
locale === 'th-th' ||
locale === 'da-dk' ||
locale === 'nl-nl' ||
locale === 'hr-hr'
locale === 'hr-hr' ||
locale === 'id-id' ||
locale === 'zh-cn'
) {
switch (locale) {
case 'zh-cn':
require('dayjs/locale/zh-cn');
break;
case 'ru':
require('dayjs/locale/ru');
break;
case 'es':
require('dayjs/locale/es');
break;
case 'fr-fr':
require('dayjs/locale/fr');
break;
case 'pt-br':
require('dayjs/locale/pt-br');
break;
case 'pt-pt':
require('dayjs/locale/pt');
break;
case 'jp-JP':
require('dayjs/locale/ja');
break;
case 'de-de':
require('dayjs/locale/de');
break;
case 'th-th':
require('dayjs/locale/th');
break;
case 'da-dk':
require('dayjs/locale/da');
break;
case 'nl-nl':
require('dayjs/locale/nl');
break;
case 'hr-hr':
require('dayjs/locale/hr');
break;
case 'id-id':
require('dayjs/locale/id');
break;
default:
break;
}
dayjs.locale(locale.split('-')[0]);
locale = locale.replace('-', '_');
strings.setLanguage(locale);
} else {
@ -52,6 +151,7 @@ strings = new Localization({
pt_pt: require('./pt_PT.js'),
es: require('./es.js'),
ua: require('./ua.js'),
jp_jp: require('./jp_JP.js'),
de_de: require('./de_DE.js'),
da_dk: require('./da_DK.js'),
cs_cz: require('./cs_CZ.js'),
@ -59,29 +159,17 @@ strings = new Localization({
nl_nl: require('./nl_NL.js'),
fr_fr: require('./fr_FR.js'),
hr_hr: require('./hr_HR.js'),
id_id: require('./id_ID.js'),
zh_cn: require('./zh_cn.js'),
});
strings.saveLanguage = lang => AsyncStorage.setItem(AppStorage.LANG, lang);
strings.transactionTimeToReadable = function(time) {
strings.transactionTimeToReadable = time => {
if (time === 0) {
return strings._.never;
}
let ago = (Date.now() - Date.parse(time)) / 1000; // seconds
if (ago / (3600 * 24) >= 30) {
ago = Math.round(ago / (3600 * 24 * 30));
return ago + ' ' + strings._.months_ago;
} else if (ago / (3600 * 24) >= 1) {
ago = Math.round(ago / (3600 * 24));
return ago + ' ' + strings._.days_ago;
} else if (ago > 3600) {
ago = Math.round(ago / 3600);
return ago + ' ' + strings._.hours_ago;
} else {
ago = Math.round(ago / 60);
return ago + ' ' + strings._.minutes_ago;
}
return dayjs(time).fromNow();
};
function removeTrailingZeros(value) {
@ -110,7 +198,12 @@ strings.formatBalance = (balance, toUnit, withFormatting = false) => {
return balance + ' ' + BitcoinUnit.BTC;
} else if (toUnit === BitcoinUnit.SATS) {
const value = new BigNumber(balance).multipliedBy(100000000);
return (withFormatting ? new Intl.NumberFormat().format(value.toString()).replace(',', ' ') : value) + ' ' + BitcoinUnit.SATS;
return (
(balance < 0 ? '-' : '') +
(withFormatting ? new Intl.NumberFormat().format(value.toString()).replace(/[^0-9]/g, ' ') : value) +
' ' +
BitcoinUnit.SATS
);
} else if (toUnit === BitcoinUnit.LOCAL_CURRENCY) {
return currency.BTCToLocalCurrency(balance);
}
@ -131,7 +224,7 @@ strings.formatBalanceWithoutSuffix = (balance, toUnit, withFormatting = false) =
const value = new BigNumber(balance).dividedBy(100000000).toFixed(8);
return removeTrailingZeros(value);
} else if (toUnit === BitcoinUnit.SATS) {
return withFormatting ? new Intl.NumberFormat().format(balance).replace(',', ' ') : balance;
return (balance < 0 ? '-' : '') + (withFormatting ? new Intl.NumberFormat().format(balance).replace(/[^0-9]/g, ' ') : balance);
} else if (toUnit === BitcoinUnit.LOCAL_CURRENCY) {
return currency.satoshiToLocalCurrency(balance);
}

223
loc/jp_JP.js

@ -0,0 +1,223 @@
module.exports = {
_: {
storage_is_encrypted: 'ウォレットは暗号化されています。復号にはパスワードが必要です。',
enter_password: 'パスワードを入力',
bad_password: 'パスワードが間違っています。',
never: 'データなし',
continue: '続行',
ok: 'OK',
},
wallets: {
select_wallet: 'ウォレット選択',
options: 'オプション',
createBitcoinWallet:
'Bitcoin ウォレットを持っていません。Lightning ウォレットへ課金する場合は Bitcoin ウォレットを新規作成するかインポートする必要があります。続行しますか?',
list: {
app_name: 'BlueWallet',
title: 'ウォレット',
header: 'ウォレットは秘密鍵(プライベートキー)とウォレットアドレスのペアで構成されています。' + 'コインを受信するために共有できます。',
add: 'ウォレットの追加',
create_a_wallet: 'ウォレットの作成',
create_a_wallet1: 'ウォレット作成は無料で',
create_a_wallet2: '好きなだけ複数作成できます',
latest_transaction: '最新の取引',
empty_txs1: 'ここに取引が表示されます',
empty_txs2: '現在は何もありません',
tap_here_to_buy: 'Bitcoin を購入するにはここをタップ',
},
reorder: {
title: 'ウォレットの並び替え',
},
add: {
title: 'ウォレットの追加',
description:
'ペーパーウォレット(WIF 形式- Wallet Import Format)をスキャンするかウォレットを新規作成できます。既定で Segwit ウォレットが作成されます。',
scan: '読取り',
create: '作成',
label_new_segwit: '新規 SegWit',
label_new_lightning: '新規 Lightning',
wallet_name: 'ウォレット名',
wallet_type: 'タイプ',
or: '又は',
import_wallet: 'ウォレットをインポート',
imported: 'インポート完了',
coming_soon: '準備中',
lightning: 'Lightning',
bitcoin: 'Bitcoin',
},
details: {
title: 'ウォレット',
address: 'アドレス',
type: 'タイプ',
label: 'ラベル',
destination: '送り先',
description: '内容',
are_you_sure: '実行しますか?',
yes_delete: 'はい、削除します',
no_cancel: 'いいえ、中止します',
delete: '削除',
save: '保存',
delete_this_wallet: 'このウォレットの削除',
export_backup: 'エクスポート / バックアップ',
buy_bitcoin: 'Bitcoin の購入',
show_xpub: 'ウォレット XPUB の表示',
},
export: {
title: 'ウォレットのエクスポート',
},
xpub: {
title: 'ウォレット XPUB',
copiedToClipboard: 'クリップボードにコピーしました。',
},
import: {
title: 'インポート',
explanation:
'ここにニモニック、秘密鍵(プライベートキー)、WIFなどを入力してください。BlueWallet が正しい形式を推測しウォレットをインポートします。',
imported: 'インポート完了',
error: 'インポートに失敗しました。入力したデータが有効か確認してください。',
success: '成功',
do_import: 'インポート',
scan_qr: 'QR コードの読み取り',
},
scanQrWif: {
go_back: '戻る',
cancel: '中止',
decoding: '解読中',
input_password: 'パスワードの入力',
password_explain: 'これは BIP38 暗号化秘密鍵です',
bad_password: 'パスワードが違います',
wallet_already_exists: '同じウォレットが既に存在します',
bad_wif: 'WIF 形式が間違っています',
imported_wif: 'WIF インポート完了 ',
with_address: ' アドレス ',
imported_segwit: 'SegWit インポート完了',
imported_legacy: 'Legacy インポート完了',
imported_watchonly: '閲覧専用 インポート完了',
},
},
transactions: {
list: {
tabBarLabel: '取引',
title: '取引',
description: 'ウォレットの送金または入金のリスト',
conf: '確認',
},
details: {
title: '取引',
from: '送り主',
to: '送り先',
copy: 'コピー',
transaction_details: '取引詳細',
show_in_block_explorer: 'Block Explorer で表示',
},
},
send: {
header: '送金',
details: {
title: '取引の作成',
amount_field_is_not_valid: '金額欄が正しくありません',
fee_field_is_not_valid: '手数料欄が正しくありません',
address_field_is_not_valid: 'アドレス欄が正しくありません',
total_exceeds_balance: '送金額が利用可能残額を超えています。',
create_tx_error: '取引作成中にエラーが発生しました。有効な送金アドレスか確認してください。',
address: 'アドレス',
amount_placeholder: '送金額 (BTC)',
fee_placeholder: '取引手数料 (BTC)',
note_placeholder: 'ラベル',
cancel: '中止',
scan: '読取り',
send: '送金',
create: '作成',
remaining_balance: '残高',
},
confirm: {
header: '確認',
sendNow: '送金実行',
},
success: {
done: '完了',
},
create: {
details: '詳細',
title: '取引の作成',
error: '取引作成でエラーが発生しました。アドレスまたは送金額を確認してください。',
go_back: '戻る',
this_is_hex: '署名されネットワークへ送信される 16 進数取引コードです',
to: '送金先',
amount: '送金額',
fee: '手数料',
tx_size: 'TX サイズ',
satoshi_per_byte: 'Satoshi/byte',
memo: 'メモ',
broadcast: '送信',
not_enough_fee: '手数料不足です。増額してください',
},
},
receive: {
header: '入金',
details: {
title: 'このアドレスを支払者と共有',
share: '共有',
copiedToClipboard: 'クリップボードにコピーしました。',
label: '概要',
create: '作成',
setAmount: '入金額',
},
},
buyBitcoin: {
header: 'Bitcoin の購入',
tap_your_address: 'タップしてアドレスをクリップボードにコピー:',
copied: 'クリップボードにコピーしました!',
},
settings: {
header: '設定',
plausible_deniability: '隠匿設定...',
storage_not_encrypted: 'ウォレット: 暗号化されていません',
storage_encrypted: 'ウォレット: 暗号化されています',
password: 'パスワード',
password_explain: 'ウォレットの復号に使用するパスワードを作成',
retype_password: 'パスワードの再入力',
passwords_do_not_match: 'パスワードが一致しません',
encrypt_storage: 'ウォレットの暗号化',
lightning_settings: 'Lightning 設定',
lightning_settings_explain:
'他の LND ノードへ接続するには LndHub をインストール後、' +
'URL を入力してください。既定の設定を使用するには空欄にします' +
'ndHub\n (lndhub.io)',
save: '保存',
about: 'BlueWallet について',
language: '言語',
currency: '通貨',
},
plausibledeniability: {
title: '隠匿設定',
help:
'BuleWallet のウォレットの復号に必要なパスワードを第三者に強要される場合、' +
'コインを安全に保護するためにメインのウォレットとは異なるパスワードで' +
'暗号化されたダミーのウォレットを作成することが可能です。' +
'第三者へ異なるパスワードを提供すれば、BlueWallet のダミーの' +
'暗号化ウォレットを復号することとなり、メインのウォレットは隠匿され' +
'コインは安全に保護されます。',
help2:
'新規のダミーのウォレットはメインと同様に機能します。少額のコインを入金しておくことで' +
'ダミーと疑われないようにすることが可能です。',
create_fake_storage: 'ダミーの暗号化ウォレットの作成',
go_back: '戻る',
create_password: 'パスワードの作成',
create_password_explanation: 'ダミーのウォレットのパスワードはメインのウォレットのパスワードと異なる必要があります。',
password_should_not_match: 'ダミーのウォレットのパスワードはメインのウォレットのパスワードと異なる必要があります。',
retype_password: 'パスワードの再入力',
passwords_do_not_match: 'パスワードが一致しません',
success: '成功',
},
lnd: {
title: '資金の管理',
choose_source_wallet: '送金元ウォレットを選択',
refill_lnd_balance: 'Lightning ウォレットへ送金',
refill: '送金',
withdraw: '引き出し',
placeholder: '入金依頼',
expired: '失効',
sameWalletAsInvoiceError: '以前作成したウォレットと同じウォレットへの支払いはできません。',
},
};

19
loc/nl_NL.js

@ -3,10 +3,6 @@ module.exports = {
storage_is_encrypted: 'Uw opslag is versleuteld. Wachtwoord is vereist om het te ontcijferen',
enter_password: 'Voer wachtwoord in',
bad_password: 'Verkeerd wachtwoord, probeer opnieuw',
months_ago: 'maanden geleden',
days_ago: 'dagen geleden',
hours_ago: 'uur geleden',
minutes_ago: 'minuten geleden',
never: 'nooit',
continue: 'Continue',
ok: 'OK',
@ -14,9 +10,10 @@ module.exports = {
wallets: {
select_wallet: 'Selecteer portemonnee',
options: 'opties',
createBitcoinWallet: 'In order to use a Lightning wallet, a Bitcoin wallet is needed to fund it. Would you like to continue anyway?',
createBitcoinWallet:
'Om een Lightning-portemonnee te kunnen gebruiken, is een Bitcoin-portemonnee nodig om deze te financieren. Wil je toch doorgaan?',
list: {
app_name: 'Blue Wallet',
app_name: 'BlueWallet',
title: 'portemonnees',
header: 'Een portemonnee vertegenwoordigt een geheime (privésleutel) en een adres' + 'dat u kunt delen om munten te ontvangen.',
add: 'Portemonnee toevoegen',
@ -35,7 +32,7 @@ module.exports = {
title: 'portemonnee toevoegen',
description:
'U kunt een back-up papieren portemonnee scannen (in WIF - Wallet Import Format) of een nieuwe portemonnee maken. Segwit-wallets worden standaard ondersteund.',
scan: 'Scannen',
scan: 'Scan',
create: 'Aanmaken',
label_new_segwit: 'Nieuwe SegWit',
label_new_lightning: 'Nieuwe Lightning',
@ -163,6 +160,7 @@ module.exports = {
share: 'delen',
copiedToClipboard: 'Gekopieerd naar het klembord.',
label: 'Omschrijving',
create: 'Create',
setAmount: 'Ontvang met bedrag',
},
},
@ -181,6 +179,12 @@ module.exports = {
retype_password: 'Geef nogmaals het wachtwoord',
passwords_do_not_match: 'Wachtwoorden komen niet overeen',
encrypt_storage: 'Versleutel opslag',
lightning_settings: 'Lightning instellingen',
lightning_settings_explain:
'Om verbinding te maken met uw eigen LND-knooppunt' +
' installeert u LndHub en zet u de URL hier in de instellingen. ' +
'Laat dit leeg om de standaard lndHub (lndhub.io) te gebruiken.',
save: 'Opslaan',
about: 'Over',
language: 'Taal',
currency: 'Valuta',
@ -214,6 +218,7 @@ module.exports = {
refill: 'Bijvullen',
withdraw: 'Opvragen',
expired: 'Verlopen',
placeholder: 'Invoice',
sameWalletAsInvoiceError: 'U kunt geen factuur betalen met dezelfde portemonnee die is gebruikt om de factuur te maken.',
},
};

14
loc/pt_BR.js

@ -3,10 +3,6 @@ module.exports = {
storage_is_encrypted: 'Os arquivos estão criptografados, é necessária uma senha',
enter_password: 'Inserir senha',
bad_password: 'Senha errada, tente outra vez',
months_ago: 'meses atrás',
days_ago: 'dias atrás',
hours_ago: 'horas atrás',
minutes_ago: 'minutos atrás',
never: 'nunca',
continue: 'Continue',
ok: 'OK',
@ -18,7 +14,7 @@ module.exports = {
list: {
tabBarLabel: 'Carteiras',
app_name: 'Blue Wallet',
app_name: 'BlueWallet',
title: 'carteiras',
header: 'Uma carteira representa um par composto de uma chave privada e um endereço que você pode .',
add: 'adicionar wallet',
@ -166,6 +162,7 @@ module.exports = {
share: 'Compartilhar',
copiedToClipboard: 'Copiado para a área de trabalho',
label: 'Descrição',
create: 'Create',
setAmount: 'Valor a receber',
},
},
@ -185,6 +182,12 @@ module.exports = {
retype_password: 'Inserir senha novamente',
passwords_do_not_match: 'Senhas não coincidem',
encrypt_storage: 'Criptografar',
lightning_settings: 'Lightning settings',
lightning_settings_explain:
'To connect to your own LND node please install LndHub' +
' and put its URL here in settings. Leave blank to use default ' +
'ndHub\n (lndhub.io)',
save: 'save',
about: 'Sobre',
language: 'Idioma',
currency: 'Moeda',
@ -214,6 +217,7 @@ module.exports = {
choose_source_wallet: 'Escolha a carteira de origem',
refill_lnd_balance: 'Recarregar a carteira Lightning',
refill: 'Recarregar',
placeholder: 'Invoice',
withdraw: 'Sacar',
expired: 'Vencido',
sameWalletAsInvoiceError: 'Você não pode pagar uma fatura com a mesma carteira que a criou.',

14
loc/pt_PT.js

@ -3,10 +3,6 @@ module.exports = {
storage_is_encrypted: 'O armazenamento está encriptado. Uma password é necessaria para desencriptar',
enter_password: 'Inserir password',
bad_password: 'pasword errada, tente novamente',
months_ago: 'months ago',
days_ago: 'days ago',
hours_ago: 'hours ago',
minutes_ago: 'minutes ago',
never: 'never',
continue: 'Continue',
ok: 'OK',
@ -17,7 +13,7 @@ module.exports = {
createBitcoinWallet: 'In order to use a Lightning wallet, a Bitcoin wallet is needed to fund it. Would you like to continue anyway?',
list: {
app_name: 'Blue Wallet',
app_name: 'BlueWallet',
title: 'wallets',
header: 'Uma wallet representa um par entre um segredo (chave privada) e um endereço' + 'que pode partilhar para receber Bitcoin.',
add: 'adicionar wallet',
@ -170,6 +166,7 @@ module.exports = {
share: 'partilhar',
copiedToClipboard: 'copiado para clip board',
label: 'Description',
create: 'Create',
setAmount: 'Receive with amount',
},
},
@ -184,6 +181,12 @@ module.exports = {
retype_password: 'Inserir password novamente',
passwords_do_not_match: 'Passwords não coincidem',
encrypt_storage: 'Encriptar',
lightning_settings: 'Lightning settings',
lightning_settings_explain:
'To connect to your own LND node please install LndHub' +
' and put its URL here in settings. Leave blank to use default ' +
'ndHub\n (lndhub.io)',
save: 'save',
about: 'Sobre',
language: 'Idioma',
currency: 'Moeda',
@ -213,6 +216,7 @@ module.exports = {
choose_source_wallet: 'Escolha a wallet',
refill_lnd_balance: 'Carregar o saldo da Lightning wallet',
refill: 'Carregar',
placeholder: 'Invoice',
withdraw: 'Transferir',
expired: 'Expired',
sameWalletAsInvoiceError: 'You can not pay an invoice with the same wallet used to create it.',

164
loc/ru.js

@ -1,82 +1,78 @@
module.exports = {
_: {
storage_is_encrypted: 'Ваше хранилище зашифровано. Введите пароль для расшифровки',
enter_password: 'Введите пароль',
bad_password: 'Неверный пароль, попробуйте еще раз',
months_ago: 'месяцев назад',
days_ago: 'дней назад',
hours_ago: 'часов назад',
minutes_ago: 'минут назад',
never: 'никогда',
continue: 'Continue',
storage_is_encrypted: 'Твоё хранилище зашифровано. Введи пароль для расшифровки',
enter_password: 'Введи пароль',
bad_password: 'Неверный пароль, попробуй еще раз',
never: 'Никогда',
continue: 'Продолжить',
ok: 'OK',
},
wallets: {
options: 'options',
select_wallet: 'Select Wallet',
createBitcoinWallet: 'In order to use a Lightning wallet, a Bitcoin wallet is needed to fund it. Would you like to continue anyway?',
options: 'Настройки',
select_wallet: 'Выбрать кошелек',
createBitcoinWallet: 'Чтобы воспользоватья кошельком Lightning, нужно сначала пополнить его с помощью кошелька Bitcoin. Продолжить?',
list: {
app_name: 'BlueWallet',
title: 'кошельки',
header: 'Кошелек это секретный (приватный) ключ, и соответствующий ему адрес на который можно получать биткоины',
title: 'Кошельки',
header: 'Кошелек - это секретный (приватный) ключ и соответствующий ему адрес на который можно получать Bitcoin',
add: 'Добавить Кошелек',
create_a_wallet: 'Создать кошелек',
create_a_wallet1: 'Это бесплатно и вы можете создать',
create_a_wallet1: 'Это бесплатно и ты можешь создать',
create_a_wallet2: 'неограниченное количество',
latest_transaction: 'последняя транзакция',
latest_transaction: 'Последняя транзакция',
empty_txs1: 'Список транзакций пока пуст',
empty_txs2: ' ',
tap_here_to_buy: 'Tap here to buy Bitcoin',
tap_here_to_buy: 'Купить Bitcoin',
},
reorder: {
title: 'Reorder Wallets',
title: 'Отсортировать кошельки',
},
add: {
title: 'добавить кошелек',
title: 'Добавить кошелек',
description:
'Вы можете отсканировать QR код (в формате WIF - Wallet Import Format), или создать новый кошелек. Segwit поддерживается по умолчанию.',
'Ты можешь отсканировать QR код (в формате WIF - Wallet Import Format) или создать новый кошелек. Segwit поддерживается по умолчанию.',
scan: 'Отсканировать',
create: 'Создать',
label_new_segwit: 'Новый SegWit',
label_new_lightning: 'Новый Lightning',
wallet_name: 'имя кошелька',
wallet_type: 'тип кошелька',
or: 'or',
wallet_name: 'Имя кошелька',
wallet_type: 'Тип кошелька',
or: 'или',
import_wallet: 'Импортировать кошелек',
imported: 'Импортирован',
coming_soon: 'Пока недоступно',
imported: 'Кошелек импортирован',
coming_soon: 'Скоро будет',
lightning: 'Lightning',
bitcoin: 'Bitcoin',
},
details: {
title: 'Информация о Кошельке',
title: 'Информация о кошельке',
address: 'Адрес',
type: 'Тип',
label: 'Метка',
delete: 'Delete',
save: 'Save',
are_you_sure: 'Вы уверены?',
delete: 'Удалить',
save: 'Сохранить',
are_you_sure: 'Точно удалить?',
yes_delete: 'Да, удалить',
destination: 'destination',
description: 'description',
destination: 'Назначение',
description: 'Описание',
no_cancel: 'Нет, отмена',
delete_this_wallet: 'Удалить этот кошелек',
export_backup: 'Экспорт / резервная копия',
buy_bitcoin: 'Buy Bitcoin',
show_xpub: 'Show wallet XPUB',
buy_bitcoin: 'Купить Bitcoin',
show_xpub: 'Показать XPUB',
},
export: {
title: 'Экспорт Кошелька',
},
xpub: {
title: 'wallet XPUB',
copiedToClipboard: 'скопировано',
title: 'XPUB кошелька',
copiedToClipboard: 'Скопировано',
},
import: {
title: 'import',
explanation: 'Напишите тут вашу мнемонику, приватный ключ, WIF, что угодно, BlueWallet постарается угадать верный формат',
imported: 'Импортирован',
title: 'Импорт',
explanation: 'Напиши тут свою мнемоническую фразу, приватный ключ, WIF - что угодно! BlueWallet постарается угадать верный формат',
imported: 'Импорт завершен',
error: 'Не удалось импортировать',
success: 'Успех',
do_import: 'Импортировать',
@ -85,8 +81,8 @@ module.exports = {
scanQrWif: {
go_back: 'Назад',
cancel: 'Отмена',
decoding: 'Декодирую',
input_password: 'Введите пароль',
decoding: 'Расшивровываю',
input_password: 'Введи пароль',
password_explain: 'Приватный ключ зашифрован по стандарту BIP38',
bad_password: 'Неверный пароль',
wallet_already_exists: 'Такой кошелек уже существует',
@ -110,40 +106,40 @@ module.exports = {
from: 'От',
to: 'Кому',
copy: 'копировать',
transaction_details: 'Transaction details',
show_in_block_explorer: 'Show in block explorer',
transaction_details: 'Детали транзакции',
show_in_block_explorer: 'Показать транзакцию в блокчейне',
},
},
send: {
header: 'Отправить',
confirm: {
header: 'Confirm',
sendNow: 'Send now',
header: 'Подтвердить',
sendNow: 'Отправить',
},
success: {
done: 'Done',
done: 'Готово',
},
details: {
title: 'Создать Транзакцию',
amount_field_is_not_valid: 'Поле не валидно',
fee_field_is_not_valid: 'Поле `комиссия` не валидно',
address_field_is_not_valid: 'Поле `адрес` не валидно',
amount_field_is_not_valid: 'Введенная сумма неверна',
fee_field_is_not_valid: 'Введенная комиссия неверна',
address_field_is_not_valid: 'Введенный адрес неверный',
receiver_placeholder: 'Адрес получателя',
amount_placeholder: 'сколько отправить (в BTC)',
fee_placeholder: 'плюс комиссия за перевод (в BTC)',
note_placeholder: 'примечание платежа',
create_tx_error: 'There was an error creating the transaction. Please, make sure the address is valid.',
create_tx_error: 'Ошибка при создании транзакции. Пожалуйста, проверь правильность адреса.',
cancel: 'Отмена',
scan: 'Скан QR',
scan: 'Отсканировать QR',
create: 'Создать',
send: 'Send',
address: 'Address',
total_exceeds_balance: 'Total amount exceeds balance.',
send: 'Отправить',
address: 'Адрес',
total_exceeds_balance: 'Общая сумма превышает баланс.',
remaining_balance: 'Остаток баланса',
},
create: {
title: 'Создать Транзакцию',
details: 'Details',
details: 'Детали',
error: 'Ошибка при создании транзакции. Неправильный адрес назначения или недостаточно средств?',
go_back: 'Назад',
this_is_hex: 'Это данные транзакции. Транзакция подписана и готова быть транслирована в сеть. Продолжить?',
@ -154,68 +150,76 @@ module.exports = {
satoshi_per_byte: 'Сатоши на байт',
memo: 'Примечание',
broadcast: 'Отправить',
not_enough_fee: 'Слишком маленькая комиссия. Увеличьте комиссию',
not_enough_fee: 'Слишком маленькая комиссия. Увеличь комиссию',
},
},
buyBitcoin: {
header: 'Buy Bitcoin',
tap_your_address: 'Tap your address to copy it to clipboard:',
copied: 'Copied to Clipboard!',
header: 'Купить Bitcoin',
tap_your_address: 'Нажми на адрес, чтобы скопировать его:',
copied: 'Скопировано',
},
receive: {
header: 'Получить',
details: {
title: 'Покажите этот адрес плательщику',
title: 'Покажи этот адрес плательщику',
share: 'Отправить',
copiedToClipboard: 'скопировано',
label: 'Description',
setAmount: 'Receive with amount',
copiedToClipboard: 'Скопировано',
label: 'Описание',
create: 'Создать',
setAmount: 'Получить сумму',
},
},
settings: {
tabBarLabel: 'Настройки',
header: 'Настройки',
plausible_deniability: 'Правдоподобное отрицание...',
plausible_deniability: 'Правдоподобная имитация...',
storage_not_encrypted: 'Хранилище: не зашифровано',
storage_encrypted: 'Хранилище: зашифровано',
password: 'Пароль',
password_explain: 'Придумайте пароль для расшифровки хранилища',
retype_password: 'Наберите пароль повторно',
password_explain: 'Придумай пароль для расшифровки хранилища',
retype_password: 'Набери пароль повторно',
passwords_do_not_match: 'Пароли не совпадают',
encrypt_storage: 'Зашифровать хранилище',
lightning_settings: 'Настройки Lightning',
lightning_settings_explain:
'Чтобы подключиться к своему узлу LND, пожалуйста, установи LndHub' +
' и добавь его URL в настройки. Оставь поле пустым, чтобы использоавать стандартный ' +
'LndHub\n (lndhub.io)',
save: 'Сохранить',
about: 'О программе',
language: 'Язык',
currency: 'Валюта',
},
plausibledeniability: {
title: 'Правдоподобное Отрицание',
title: 'Правдоподобная имитация',
help:
'При определенных обстоятельствах вас могут вынудить раскрыть пароль. ' +
'Чтобы сохранить ваши биткоины в безопасности, BlueWallet может создать ' +
'еще одно зашифрованое хранилище, с другим паролем. Под давлением, ' +
'вы можете раскрыть третьим лицам этот пароль. Если ввести этот пароль в ' +
'При определенных обстоятельствах тебя могут вынудить раскрыть пароль. ' +
'Чтобы сохранить свой Bitcoin в безопасности, BlueWallet может создать ' +
'еще одно зашифрованое хранилище, с другим паролем. В случае шантажа ' +
'ты можешь раскрыть третьим лицам этот пароль. Если ввести этот пароль в ' +
"BlueWallet, разблокируется 'фальшивое' хранилище. Это будет выглядеть " +
'правдоподобно для третьих лиц, но при этом сохранит ваше основное хранилище ' +
биткоинами в безопасности.',
Bitcoin в безопасности.',
help2:
'Новое хранилище будет полностью функциональным и вы даже можете хранить на нем немного биткоинов ' +
'Новое хранилище будет полностью функциональным и ты даже можешь хранить на нем немного Bitcoin, ' +
'чтобы это выглядело более правдоподобно.',
create_fake_storage: 'Создать фальшивое хранилище',
go_back: 'Назад',
create_password: 'Придумайте пароль',
create_password: 'Придумай пароль',
create_password_explanation: 'Пароль для фальшивого хранилища не должен быть таким же как основной пароль',
password_should_not_match: 'Пароль для фальшивого хранилища не должен быть таким же как основной пароль',
retype_password: 'Наберите пароль повторно',
passwords_do_not_match: 'Пароли не совпадают, попробуйте еще раз',
success: 'Операция успешна',
retype_password: 'Набери пароль повторно',
passwords_do_not_match: 'Пароли не совпадают, попробуй еще раз',
success: 'Готово',
},
lnd: {
title: 'мои средства',
choose_source_wallet: 'Выберите с какого кошелька',
title: 'Мои средства',
choose_source_wallet: 'Выбери с какого кошелька',
refill_lnd_balance: 'Пополнить баланс Lightning кошелька',
refill: 'Пополнить',
withdraw: 'Вывести',
expired: 'Expired',
sameWalletAsInvoiceError: 'You can not pay an invoice with the same wallet used to create it.',
expired: 'Истекший',
placeholder: 'Invoice',
sameWalletAsInvoiceError: 'Ты не можешь оплатить счет тем же кошельком, который ты использовал для его создания.',
},
};

22
loc/th_TH.js

@ -3,19 +3,15 @@ module.exports = {
storage_is_encrypted: 'ที่เก็บข้อมูลของคุณถูกเข้ารหัส. ต้องการรหัสผ่านเพื่อถอดรหัส',
enter_password: 'กรุณาใส่รหัสผ่าน',
bad_password: 'รหัสผ่านไม่ถูกต้อง กรุณาใส่รหัสผ่านอีกครั้ง',
months_ago: 'เดือนที่แล้ว',
days_ago: 'วันที่แล้ว',
hours_ago: 'ชั่วโมงที่แล้ว',
minutes_ago: 'นาทีที่แล้ว',
never: 'ไม่เคย',
continue: 'Continue',
ok: 'OK',
continue: 'ต่อไป',
ok: 'ตกลง',
},
wallets: {
select_wallet: 'เลือกกระเป๋าสตางค์',
options: 'ทางเลือก',
createBitcoinWallet: 'In order to use a Lightning wallet, a Bitcoin wallet is needed to fund it. Would you like to continue anyway?',
createBitcoinWallet:
'ในการใช้งานกระเป๋าสตางค์ไลท์นิง คุณจำเป็นต้องมีกระเป๋าสตางค์บิตคอยน์. คุณต้องการสร้างกระเป๋าสตางค์บิตคอยน์หรือไม่?',
list: {
app_name: 'บูลวอลเล็ต',
title: 'กระเป๋าสตางค์',
@ -163,6 +159,7 @@ module.exports = {
share: 'แชร์',
copiedToClipboard: 'ก๊อปปี้ไปที่คลิปบอร์ดแล้ว.',
label: 'คำอธิบาย',
create: 'สร้าง',
setAmount: 'รับด้วยจำนวน',
},
},
@ -181,6 +178,12 @@ module.exports = {
retype_password: 'ใส่รหัสผ่านอีกครั้ง',
passwords_do_not_match: 'รหัสผ่านไม่ตรงกัน',
encrypt_storage: 'เข้ารหัสที่เก็บข้อมูล',
lightning_settings: 'การตั้งค่าไลท์นิง',
lightning_settings_explain:
'กรณีที่ต้องการต่อเชื่อมไปยังไลท์นิงโนดของท่านเอง กรุณาติดตั้ง LndHub ของท่าน' +
' และไส่ URL ที่นี่. ระบบจะใช้ LndHub ของ lndhub.io ในกรณืที่ท่าน ' +
' ไม่ตั้งค่าใดๆ',
save: 'บันทึก',
about: 'เกี่ยวกับ',
language: 'ภาษา',
currency: 'เงินตรา',
@ -203,7 +206,7 @@ module.exports = {
password_should_not_match: 'รหัสผ่านสำหรับที่เก็บข้อมูลเทียมไม่ควรตรงกับรหัสผ่านที่ใช้กับที่เก็บข้อมูลเทียมจริง',
retype_password: 'ใส่รหัสผ่านอีกครั้ง ใส่รหัสผ่านอีกครั้ง',
passwords_do_not_match: 'รหัสผ่านไม่ตรงกัน ',
success: 'Success',
success: 'สำเร็จ',
},
lnd: {
title: 'จัดการเงิน',
@ -211,6 +214,7 @@ module.exports = {
refill_lnd_balance: 'เติมกระเป๋าสตางค์ไลท์นิง',
refill: 'เติม',
withdraw: 'ถอน',
placeholder: 'Invoice',
expired: 'หมดอายุแล้ว',
sameWalletAsInvoiceError: 'คุณไม่สามารถจ่ายใบแจ้งหนี้นี้ด้วยกระเป๋าสตางค์อันเดียวกันกับที่ใช้สร้างมัน.',
},

12
loc/ua.js

@ -3,10 +3,6 @@ module.exports = {
storage_is_encrypted: 'Ваше сховище зашифроване. Введіть пароль для розшифровки',
enter_password: 'Введіть пароль',
bad_password: 'Невірний пароль, спробуйте ще раз',
months_ago: 'місяців тому',
days_ago: 'днів тому',
hours_ago: 'часів тому',
minutes_ago: 'мінут тому',
never: 'ніколи',
continue: 'Continue',
ok: 'OK',
@ -164,6 +160,7 @@ module.exports = {
share: 'Відправити',
copiedToClipboard: 'Зкопіювано',
label: 'Description',
create: 'Create',
setAmount: 'Receive with amount',
},
},
@ -183,6 +180,12 @@ module.exports = {
retype_password: 'Наберіть пароль ще раз',
passwords_do_not_match: 'Паролі не збігаються',
encrypt_storage: 'Зашифрувати сховище',
lightning_settings: 'Lightning settings',
lightning_settings_explain:
'To connect to your own LND node please install LndHub' +
' and put its URL here in settings. Leave blank to use default ' +
'ndHub\n (lndhub.io)',
save: 'save',
about: 'Про програму',
language: 'Мова',
currency: 'Валюта',
@ -214,6 +217,7 @@ module.exports = {
choose_source_wallet: 'Оберіть гаманець с якого слати',
refill_lnd_balance: 'Збільшити баланс Lightning гаманця',
refill: 'Поповнити',
placeholder: 'Invoice',
withdraw: 'Вивести',
expired: 'Expired',
sameWalletAsInvoiceError: 'You can not pay an invoice with the same wallet used to create it.',

215
loc/zh_cn.js

@ -0,0 +1,215 @@
module.exports = {
_: {
storage_is_encrypted: '你的信息已经被加密, 请输入密码解密',
enter_password: '输入密码',
bad_password: '密码无效,请重试',
never: '不',
continue: '继续',
ok: '好的',
},
wallets: {
select_wallet: '选择钱包',
options: '选项',
createBitcoinWallet: '您当前没有bitcoin钱包. 为了支持闪电钱包, 我们需要创建或者导入一个比特币钱包. 是否需要继续?',
list: {
app_name: 'BlueWallet',
title: '钱包',
header: '一个钱包代表一对的私钥和地址' + '你可以通过分享收款.',
add: '添加钱包',
create_a_wallet: '创建一个钱包',
create_a_wallet1: '创建钱包是免费的,你可以',
create_a_wallet2: '想创建多少就创建多少个',
latest_transaction: '最近的转账',
empty_txs1: '你的转账信息将展示在这里',
empty_txs2: '当前无信息',
tap_here_to_buy: '点击购买比特币',
},
reorder: {
title: '重新排列钱包',
},
add: {
title: '添加钱包',
description: '你可以扫描你的纸质备份钱包 (WIF格式), 或者创建一个新钱包. 默认支持隔离见证钱包',
scan: '扫描',
create: '创建',
label_new_segwit: '新隔离见证(Segwit)',
label_new_lightning: '新闪电',
wallet_name: '钱包名称',
wallet_type: '类型',
or: '或',
import_wallet: '导入钱包',
imported: '已经导入',
coming_soon: '即将来临',
lightning: '闪电',
bitcoin: '比特币',
},
details: {
title: '钱包',
address: '地址',
type: '类型',
label: '标签',
destination: '目的',
description: '描述',
are_you_sure: '你确认么?',
yes_delete: '是的,删除',
no_cancel: '不,取消',
delete: '删除',
save: '保存',
delete_this_wallet: '删除这个钱包',
export_backup: '导出备份',
buy_bitcoin: '购买比特币',
show_xpub: '展示钱包 XPUB',
},
export: {
title: '钱包导出',
},
xpub: {
title: '钱包 XPUB',
copiedToClipboard: '复制到粘贴板.',
},
import: {
title: '导入',
explanation: '输入你的助记词,私钥或者WIF, 或者其他格式的数据. BlueWallet将尽可能的自动识别数据格式并导入钱包',
imported: '已经导入',
error: '导入失败,请确认你提供的信息是有效的',
success: '成功',
do_import: '导入',
scan_qr: '或扫面二维码',
},
scanQrWif: {
go_back: '回退',
cancel: '取消',
decoding: '解码中',
input_password: '输入密码',
password_explain: '这是一个BIP38加密的私钥',
bad_password: '密码错误',
wallet_already_exists: '当前钱包已经存在',
bad_wif: 'WIF格式无效',
imported_wif: 'WIF已经导入',
with_address: ' 地址为',
imported_segwit: 'SegWit已经导入',
imported_legacy: 'Legacy已经导入',
imported_watchonly: '导入只读',
},
},
transactions: {
list: {
tabBarLabel: '转账',
title: '转账',
description: '当前所有钱包的转入和转出记录',
conf: '配置',
},
details: {
title: '转账',
from: '输入',
to: '输出',
copy: '复制',
transaction_details: '转账详情',
show_in_block_explorer: '区块浏览器展示',
},
},
send: {
header: '发送',
details: {
title: '创建交易',
amount_field_is_not_valid: '金额格式无效',
fee_field_is_not_valid: '费用格式无效',
address_field_is_not_valid: '地址内容无效',
total_exceeds_balance: '余额不足',
create_tx_error: '创建交易失败. 请确认地址格式正确.',
address: '地址',
amount_placeholder: '发送金额(in BTC)',
fee_placeholder: '手续费用 (in BTC)',
note_placeholder: '消息',
cancel: '取消',
scan: '扫描',
send: '发送',
create: '创建',
remaining_balance: '剩余金额',
},
confirm: {
header: '确认',
sendNow: '现在发送',
},
success: {
done: '完成',
},
create: {
details: '详情',
title: '创建详情',
error: '创建交易失败. 无效地址或金额?',
go_back: '回退',
this_is_hex: '这个是交易的十六进制数据, 签名并广播到全网络.',
to: '到',
amount: '金额',
fee: '手续费',
tx_size: '交易大小',
satoshi_per_byte: '葱每byte',
memo: '消息',
broadcast: '广播',
not_enough_fee: '手续费不够,请增加手续费',
},
},
receive: {
header: '收款',
details: {
title: '分享这个地址给付款人',
share: '分享',
copiedToClipboard: '复制到粘贴板.',
label: '描述',
create: '创建',
setAmount: '收款金额',
},
},
buyBitcoin: {
header: '购买比特币',
tap_your_address: '点击地址复制到粘贴板:',
copied: '复制到粘贴板!',
},
settings: {
header: '设置',
plausible_deniability: '可否认性...',
storage_not_encrypted: '存储:未加密',
storage_encrypted: '存储:加密中',
password: '密码',
password_explain: '创建你的加密密码',
retype_password: '再次输入密码',
passwords_do_not_match: '两次输入密码不同',
encrypt_storage: '加密存储',
lightning_settings: '闪电网络设置',
lightning_settings_explain: '如要要连接你自己的闪电节点请安装LndHub' + ' 并把url地址输入到下面. 空白将使用默认的LndHub (lndhub.io)',
save: '保存',
about: '关于',
language: '语言',
currency: '货币',
},
plausibledeniability: {
title: '可否认性',
help:
'在某些情况下, 你不得不暴露 ' +
'密码. 为了让你的比特币更加安全, BlueWallet可以创建一些 ' +
'加密空间, 用不同的密码. 在压力之下, ' +
'你可以暴露这个钱包密码. 再次进入 ' +
'BlueWallet, 我们会解锁一些虚拟空间. 对第三方来说看上去' +
'是合理的, 但会偷偷的帮你保证主钱包的安全 ' +
'币也就安全了.',
help2: '新的空间具备完整的功能,你可以存在 ' + '少量的金额在里面.',
create_fake_storage: '创建虚拟加密存储',
go_back: '回退',
create_password: '创建密码',
create_password_explanation: '虚拟存储空间密码不能和主存储空间密码相同',
password_should_not_match: '虚拟存储空间密码不能和主存储空间密码相同',
retype_password: '重输密码',
passwords_do_not_match: '两次输入密码不同,请重新输入',
success: '成功',
},
lnd: {
title: '配置资金支持',
choose_source_wallet: '选择一个资金源钱包',
refill_lnd_balance: '给闪电钱包充值',
refill: '充值',
withdraw: '提取',
expired: '超时',
sameWalletAsInvoiceError: '你不能用创建账单的钱包去支付该账单',
},
};

8
models/fiatUnit.js

@ -1,7 +1,9 @@
export const FiatUnit = Object.freeze({
USD: { endPointKey: 'USD', symbol: '$', locale: 'en-US' },
AUD: { endPointKey: 'AUD', symbol: '$', locale: 'en-AU' },
BRL: { endPointKey: 'BRL', symbol: 'R$', locale: 'pt-BR' },
CAD: { endPointKey: 'CAD', symbol: '$', locale: 'en-CA' },
CHF: { endPointKey: 'CHF', symbol: 'CHF', locale: 'de-CH' },
CZK: { endPointKey: 'CZK', symbol: 'Kč', locale: 'cs-CZ' },
CNY: { endPointKey: 'CNY', symbol: '¥', locale: 'zh-CN' },
EUR: { endPointKey: 'EUR', symbol: '€', locale: 'en-EN' },
@ -9,8 +11,14 @@ export const FiatUnit = Object.freeze({
HRK: { endPointKey: 'HRK', symbol: 'HRK', locale: 'hr-HR' },
INR: { endPointKey: 'INR', symbol: '₹', locale: 'hi-HN' },
JPY: { endPointKey: 'JPY', symbol: '¥', locale: 'ja-JP' },
MXN: { endPointKey: 'MXN', symbol: '$', locale: 'es-MX' },
MYR: { endPointKey: 'MYR', symbol: 'RM', locale: 'ms-MY' },
NZD: { endPointKey: 'NZD', symbol: '$', locale: 'en-NZ' },
PLN: { endPointKey: 'PLN', symbol: 'zł', locale: 'pl-PL' },
RUB: { endPointKey: 'RUB', symbol: '₽', locale: 'ru-RU' },
SGD: { endPointKey: 'SGD', symbol: 'S$', locale: 'zh-SG' },
SEK: { endPointKey: 'SEK', symbol: 'kr', locale: 'sv-SE' },
THB: { endPointKey: 'THB', symbol: '฿', locale: 'th-TH' },
VEF: { endPointKey: 'VEF', symbol: 'Bs.', locale: 'es-VE' },
ZAR: { endPointKey: 'ZAR', symbol: 'R', locale: 'en-ZA' },
});

2
models/networkTransactionFees.js

@ -1,6 +1,8 @@
import Frisbee from 'frisbee';
export class NetworkTransactionFee {
static StorageKey = 'NetworkTransactionFee';
constructor(fastestFee = 1, halfHourFee = 1, hourFee = 1) {
this.fastestFee = fastestFee;
this.halfHourFee = halfHourFee;

13535
package-lock.json

File diff suppressed because it is too large

150
package.json

@ -1,28 +1,29 @@
{
"name": "BlueWallet",
"version": "3.5.6",
"version": "3.8.0",
"devDependencies": {
"babel-eslint": "^8.2.6",
"babel-jest": "23.6.0",
"eslint": "^4.19.1",
"eslint-plugin-babel": "^4.1.2",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-react": "^7.11.1",
"jest": "23.6.0",
"metro-react-native-babel-preset": "^0.49.1",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.0.0",
"eslint": "^5.13.0",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-import": "^2.15.0",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.12.3",
"jest": "^24.0.0",
"metro-react-native-babel-preset": "^0.51.1",
"prettier-eslint-cli": "^4.7.1",
"react-test-renderer": "^16.7.0",
"rn-nodeify": "github:mvayngrib/rn-nodeify"
"rn-nodeify": "github:tradle/rn-nodeify"
},
"scripts": {
"prepare": "./patches/fix_mangle.sh; git apply patches/minifier.js.patch; git apply patches/minify.js.patch; git apply patches/transaction_builder.js.patch; git apply ./patches/transaction.js.patch; test -f sentry.sh && ./sentry.sh || true",
"clean": "rm -r -f node_modules/",
"prepare": "./patches/fix_mangle.sh; git apply patches/minifier.js.patch; git apply patches/minify.js.patch; git apply patches/transaction_builder.js.patch; git apply ./patches/transaction.js.patch",
"clean": "cd android/; ./gradlew clean; cd ..; rm -r -f /tmp/metro-cache/; rm -r -f node_modules/; npm cache clean --force; npm i; npm start -- --reset-cache",
"releasenotes2json": "./release-notes.sh > release-notes.txt; node -e 'console.log(JSON.stringify(require(\"fs\").readFileSync(\"release-notes.txt\", \"utf8\")));' > release-notes.json",
"start": "node node_modules/react-native/local-cli/cli.js start",
"android": "react-native run-android",
"ios": "react-native run-ios",
"postinstall": "./node_modules/.bin/rn-nodeify --install buffer,events,process,stream,util,inherits,fs,path --hack",
"postinstall": "./node_modules/.bin/rn-nodeify --install buffer,events,process,stream,util,inherits,fs,path --hack; npm run releasenotes2json",
"test": "npm run unit && npm run jest && npm run lint",
"jest": "node node_modules/jest/bin/jest.js",
"lint": "./node_modules/.bin/eslint *.js screen/**/*.js screen/ class/ models/ loc/ --fix",
@ -35,68 +36,65 @@
}
},
"dependencies": {
"@babel/preset-env": "^7.2.3",
"apollo-boost": "^0.1.23",
"asyncstorage-down": "^3.1.1",
"bignumber.js": "^7.0.0",
"bip21": "^2.0.2",
"bip39": "^2.5.0",
"bitcoinjs-lib": "^3.3.2",
"buffer": "^5.2.1",
"buffer-reverse": "^1.0.1",
"crypto-js": "^3.1.9-1",
"dayjs": "^1.7.8",
"eslint-config-prettier": "^2.10.0",
"eslint-config-standard": "^12.0.0",
"eslint-config-standard-react": "^7.0.2",
"eslint-plugin-prettier": "^2.6.2",
"eslint-plugin-standard": "^4.0.0",
"events": "^1.1.1",
"frisbee": "^1.6.4",
"graphql": "^14.0.2",
"intl": "^1.2.5",
"isaac": "0.0.5",
"mocha": "^5.2.0",
"node-libs-react-native": "^1.0.1",
"path-browserify": "0.0.0",
"prettier": "^1.14.2",
"process": "^0.11.10",
"prop-types": "^15.6.2",
"react": "^16.7.0",
"react-apollo": "^2.3.3",
"react-localization": "^1.0.10",
"react-native": "^0.57.8",
"react-native-camera": "^1.6.4",
"react-native-custom-qr-codes": "^2.0.0",
"react-native-device-info": "^0.24.3",
"react-native-elements": "^0.19.0",
"react-native-flexi-radio-button": "^0.2.2",
"react-native-fs": "^2.13.3",
"react-native-gesture-handler": "^1.0.12",
"react-native-google-analytics-bridge": "^6.1.2",
"react-native-haptic-feedback": "^1.4.2",
"react-native-level-fs": "^3.0.1",
"react-native-linear-gradient": "^2.5.3",
"react-native-material-dropdown": "^0.11.1",
"react-native-modal": "^7.0.2",
"react-native-permissions": "^1.1.1",
"react-native-prompt-android": "^0.3.4",
"react-native-qrcode": "^0.2.7",
"react-native-randombytes": "^3.5.1",
"react-native-sentry": "^0.39.1",
"react-native-snap-carousel": "^3.7.4",
"@babel/preset-env": "7.3.1",
"@remobile/react-native-qrcode-local-image": "1.0.4",
"bignumber.js": "8.0.2",
"bip21": "2.0.2",
"bip39": "2.5.0",
"bitcoinjs-lib": "3.3.2",
"buffer": "5.2.1",
"buffer-reverse": "1.0.1",
"crypto-js": "3.1.9-1",
"dayjs": "1.8.6",
"electrum-client": "git+https://github.com/Overtorment/node-electrum-client.git",
"eslint-config-prettier": "4.0.0",
"eslint-config-standard": "12.0.0",
"eslint-config-standard-react": "7.0.2",
"eslint-plugin-prettier": "3.0.1",
"eslint-plugin-standard": "4.0.0",
"frisbee": "2.0.5",
"intl": "1.2.5",
"mocha": "5.2.0",
"node-libs-react-native": "1.0.1",
"path-browserify": "1.0.0",
"prettier": "1.16.3",
"process": "0.11.10",
"prop-types": "15.6.2",
"react": "16.7.0",
"react-localization": "1.0.10",
"react-native": "0.58.1",
"react-native-camera": "1.10.0",
"react-native-device-info": "0.26.1",
"react-native-elements": "0.19.0",
"react-native-flexi-radio-button": "0.2.2",
"react-native-fs": "2.13.3",
"react-native-gesture-handler": "1.0.15",
"react-native-google-analytics-bridge": "7.0.0",
"react-native-haptic-feedback": "1.5.0",
"react-native-image-picker": "0.28.0",
"react-native-level-fs": "3.0.1",
"react-native-linear-gradient": "2.5.3",
"react-native-modal": "7.0.2",
"react-native-permissions": "1.1.1",
"react-native-prompt-android": "0.3.4",
"react-native-qrcode-svg": "5.1.2",
"react-native-randombytes": "3.5.2",
"react-native-rate": "1.1.6",
"react-native-sentry": "0.41.1",
"react-native-snap-carousel": "3.7.5",
"react-native-sortable-list": "0.0.22",
"react-native-svg": "^8.0.10",
"react-native-vector-icons": "^6.0.2",
"react-navigation": "^3.0.9",
"react-test-render": "^1.1.1",
"readable-stream": "^1.1.14",
"request-promise-native": "^1.0.5",
"secure-random": "^1.1.1",
"stream-browserify": "^1.0.0",
"util": "^0.10.4",
"whatwg-fetch": "^2.0.4",
"wif": "^2.0.1"
"react-native-svg": "9.1.1",
"react-native-tcp": "3.3.0",
"react-native-vector-icons": "6.2.0",
"react-native-webview": "4.1.0",
"react-native-wkwebview-reborn": "2.0.0",
"react-navigation": "3.1.2",
"react-test-render": "1.1.1",
"readable-stream": "3.1.1",
"secure-random": "1.1.1",
"stream-browserify": "2.0.2",
"util": "0.11.1",
"wif": "2.0.1"
},
"react-native": {
"path": "path-browserify",

1
patches/fix_mangle.sh

@ -2,3 +2,4 @@
grep -rl "mangle: { toplevel: true }" ./node_modules/ | xargs sed -i '' -e "s/mangle: { toplevel: true }/mangle: false/g" || true
grep -rl "mangle: {toplevel: true}" ./node_modules/ | xargs sed -i '' -e "s/mangle: {toplevel: true}/mangle: false/g" || true
grep -rl "BASE_MAP.fill(255)" ./node_modules/ | xargs sed -i '' -e "s/BASE_MAP.fill(255)/for (let c = 0 ; c< 256; c++) BASE_MAP[c] = 255;/g" || true
echo fix_mangle.sh done

16
patches/minifier.js.patch

@ -1,15 +1,15 @@
diff --git a/node_modules/metro-minify-uglify/src/minifier.js b/node_modules/metro-minify-uglify/src/minifier.js
index 021df4c..dcea186 100644
index b703ee4..fadc077 100644
--- a/node_modules/metro-minify-uglify/src/minifier.js
+++ b/node_modules/metro-minify-uglify/src/minifier.js
@@ -44,9 +44,7 @@ function minify(_ref) {
reserved = _ref.reserved,
@@ -67,9 +67,7 @@ function minify(_ref) {
config = _ref.config;
const options = _extends({}, config, {
- mangle: _extends({}, config.mangle, {
const options = _objectSpread({}, config, {
- mangle: _objectSpread({}, config.mangle, {
- reserved
- }),
+ mangle: false, // !!!!!!!!!!!!!!!!
sourceMap: _extends({}, config.sourceMap, {
+ mangle: false, // !!!!!!!!!!!!!!!!!!!!!!!!
sourceMap: _objectSpread({}, config.sourceMap, {
content: map
})

501
screen/lnd/browser.js

@ -0,0 +1,501 @@
import React, { Component } from 'react';
import { TouchableOpacity, ActivityIndicator, View, Platform, Alert, Dimensions } from 'react-native';
import { WebView } from 'react-native-webview';
import WKWebView from 'react-native-wkwebview-reborn';
import { BlueNavigationStyle, SafeBlueArea } from '../../BlueComponents';
import { FormInput } from 'react-native-elements';
import Ionicons from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types';
const { width } = Dimensions.get('window');
let processedInvoices = {};
let lastTimeTriedToPay = 0;
/// ///////////////////////////////////////////////////////////////////////
// this code has no use in RN, it gets copypasted in webview injected code
//
let bluewalletResponses = {};
// eslint-disable-next-line
var webln = {
enable: function() {
window.postMessage(JSON.stringify({ enable: true }));
return new Promise(function(resolve, reject) {
resolve(true);
});
},
getInfo: function() {
window.postMessage('getInfo');
return new Promise(function(resolve, reject) {
reject(new Error('not implemented'));
});
},
sendPayment: function(paymentRequest) {
window.postMessage(JSON.stringify({ sendPayment: paymentRequest }));
return new Promise(function(resolve, reject) {
/* nop. intentionally, forever hang promise.
lapp page usually asynchroniously checks payment itself, via ajax,
so atm there's no need to pass payment preimage from RN to webview and fullfill promise.
might change in future */
});
},
makeInvoice: function(RequestInvoiceArgs) {
var id = Math.random();
window.postMessage(JSON.stringify({ makeInvoice: RequestInvoiceArgs, id: id }));
return new Promise(function(resolve, reject) {
var interval = setInterval(function() {
if (bluewalletResponses[id]) {
clearInterval(interval);
resolve(bluewalletResponses[id]);
}
}, 1000);
});
},
signMessage: function() {
window.postMessage('signMessage');
return new Promise(function(resolve, reject) {
reject(new Error('not implemented'));
});
},
verifyMessage: function() {
window.postMessage('verifyMessage');
return new Promise(function(resolve, reject) {
reject(new Error('not implemented'));
});
},
};
// end injected code
/// /////////////////
/// /////////////////
let alreadyInjected = false;
const injectedParadise = `
/* rules:
no 'let', only 'var'
no arrow functions
globals without 'var'
should work if compressed to single line
*/
/* this is a storage of responses from OUTER code (react native)
it gets written by message bus handler callback:
webview makes a call through bus to RN, each request with a unique ID.
RN processes the request from the bus, and posts response to the bus, with the same ID.
webview callback handler writes it in this hashmap. Then, some other code that patiently
waits for a response will see that the answer with such ID is present, and will fulfill a promise */
bluewalletResponses = {};
/* this is injected WEBLN provider */
webln = {
enable : function () {
window.postMessage(JSON.stringify({'enable': true}));
return new Promise(function(resolve, reject){
resolve(true);
})
},
getInfo : function () {
window.postMessage('getInfo');
return new Promise(function(resolve, reject){
reject('not implemented');
})
},
sendPayment: function(paymentRequest) {
window.postMessage(JSON.stringify({ sendPayment: paymentRequest }));
return new Promise(function(resolve, reject) {
/* nop. intentionally, forever hang promise.
lapp page usually asynchroniously checks payment itself, via ajax,
so atm there's no need to pass payment preimage from RN to webview and fullfill promise.
might change in future */
});
},
makeInvoice: function (RequestInvoiceArgs) {
var id = Math.random();
window.postMessage(JSON.stringify({makeInvoice: RequestInvoiceArgs, id: id}));
return new Promise(function(resolve, reject) {
var interval = setInterval(function () {
if (bluewalletResponses[id]) {
clearInterval(interval);
resolve(bluewalletResponses[id]);
}
}, 1000);
});
},
signMessage: function () {
window.postMessage('signMessage');
return new Promise(function(resolve, reject){
reject('not implemented');
})
},
verifyMessage: function () {
window.postMessage('verifyMessage');
return new Promise(function(resolve, reject){
reject('not implemented');
})
},
};
/* end WEBLN */
/* listening to events that might come from RN: */
document.addEventListener("message", function(event) {
window.postMessage("inside webview, received post message: " + event.data);
var json;
try {
json = JSON.parse(event.data);
} catch (_) {}
if (json && json.bluewalletResponse) {
/* this is an answer to one of our inside-webview calls.
we store it in answers hashmap for someone who cares about it */
bluewalletResponses[json.id] = json.bluewalletResponse
}
}, false);
function tryToPay(invoice) {
window.postMessage(JSON.stringify({sendPayment:invoice}));
}
/* for non-webln compatible pages we do it oldschool,
searching for all bolt11 manually */
setInterval(function() {
window.postMessage('interval');
var searchText = "lnbc";
var aTags = document.getElementsByTagName("span");
var i;
for (i = 0; i < aTags.length; i++) {
if (aTags[i].textContent.indexOf(searchText) === 0) {
tryToPay(aTags[i].textContent);
}
}
/* ------------------------- */
aTags = document.getElementsByTagName("input");
for (i = 0; i < aTags.length; i++) {
if (aTags[i].value.indexOf(searchText) === 0) {
tryToPay(aTags[i].value);
}
}
/* ------------------------- */
aTags = document.getElementsByTagName("a");
searchText = "lightning:lnbc";
for (i = 0; i < aTags.length; i++) {
var href = aTags[i].getAttribute('href') + '';
if (href.indexOf(searchText) === 0) {
tryToPay(href.replace('lightning:', ''));
}
}
}, 1000);
`;
export default class Browser extends Component {
static navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true),
title: 'Lapp Browser',
headerLeft: null,
});
constructor(props) {
super(props);
if (!props.navigation.getParam('fromSecret')) throw new Error('Invalid param');
if (!props.navigation.getParam('fromWallet')) throw new Error('Invalid param');
this.state = {
url: 'https://bluewallet.io/marketplace/',
pageIsLoading: false,
fromSecret: props.navigation.getParam('fromSecret'),
fromWallet: props.navigation.getParam('fromWallet'),
};
}
renderWebView = () => {
if (Platform.OS === 'android') {
return (
<WebView
ref={ref => (this.webview = ref)}
source={{ uri: this.state.url }}
onMessage={e => {
// this is a handler which receives messages sent from within the browser
console.log('---- message from the bus:', e.nativeEvent.data);
let json = false;
try {
json = JSON.parse(e.nativeEvent.data);
} catch (_) {}
// message from browser has ln invoice
if (json && json.sendPayment) {
// checking that already asked about this invoice:
if (processedInvoices[json.sendPayment]) {
return;
} else {
// checking that we do not trigger alert too often:
if (+new Date() - lastTimeTriedToPay < 3000) {
return;
}
lastTimeTriedToPay = +new Date();
//
processedInvoices[json.sendPayment] = 1;
}
Alert.alert(
'Page',
'This page asks for permission to pay an invoice',
[
{ text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
{
text: 'Pay',
onPress: () => {
console.log('OK Pressed');
this.props.navigation.navigate({
routeName: 'ScanLndInvoice',
params: {
uri: json.sendPayment,
fromSecret: this.state.fromSecret,
},
});
},
},
],
{ cancelable: false },
);
}
if (json && json.makeInvoice) {
let amount = Math.max(+json.makeInvoice.minimumAmount, +json.makeInvoice.maximumAmount, +json.makeInvoice.defaultAmount);
Alert.alert(
'Page',
'This page wants to pay you ' + amount + ' sats (' + json.makeInvoice.defaultMemo + ')',
[
{ text: 'No thanks', onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
{
text: 'Accept',
onPress: async () => {
/** @type {LightningCustodianWallet} */
const fromWallet = this.state.fromWallet;
const payreq = await fromWallet.addInvoice(amount, json.makeInvoice.defaultMemo || ' ');
this.webview.postMessage(JSON.stringify({ bluewalletResponse: { paymentRequest: payreq }, id: json.id }));
},
},
],
{ cancelable: false },
);
}
if (json && json.enable) {
console.log('webln enabled');
this.setState({ weblnEnabled: true });
}
}}
onLoadStart={e => {
alreadyInjected = false;
console.log('load start');
this.setState({ pageIsLoading: true, weblnEnabled: false });
}}
onLoadEnd={e => {
console.log('load end');
this.setState({ url: e.nativeEvent.url, pageIsLoading: false });
}}
onLoadProgress={e => {
console.log('progress:', e.nativeEvent.progress);
if (!alreadyInjected && e.nativeEvent.progress > 0.5) {
this.webview.injectJavaScript(injectedParadise);
alreadyInjected = true;
console.log('injected');
}
}}
/>
);
} else if (Platform.OS === 'ios') {
return (
<WKWebView
ref={ref => (this.webview = ref)}
source={{ uri: this.state.url }}
injectJavaScript={injectedParadise}
onMessage={e => {
// this is a handler which receives messages sent from within the browser
console.log('---- message from the bus:', e.nativeEvent.data);
let json = false;
try {
json = JSON.parse(e.nativeEvent.data);
} catch (_) {}
// message from browser has ln invoice
if (json && json.sendPayment) {
// checking that we do not trigger alert too often:
if (+new Date() - lastTimeTriedToPay < 3000) {
return;
}
lastTimeTriedToPay = +new Date();
// checking that already asked about this invoice:
if (processedInvoices[json.sendPayment]) {
return;
} else {
processedInvoices[json.sendPayment] = 1;
}
Alert.alert(
'Page',
'This page asks for permission to pay an invoice',
[
{ text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
{
text: 'Pay',
onPress: () => {
console.log('OK Pressed');
this.props.navigation.navigate({
routeName: 'ScanLndInvoice',
params: {
uri: json.sendPayment,
fromSecret: this.state.fromSecret,
},
});
},
},
],
{ cancelable: false },
);
}
if (json && json.makeInvoice) {
let amount = Math.max(+json.makeInvoice.minimumAmount, +json.makeInvoice.maximumAmount, +json.makeInvoice.defaultAmount);
Alert.alert(
'Page',
'This page wants to pay you ' + amount + ' sats (' + json.makeInvoice.defaultMemo + ')',
[
{ text: 'No thanks', onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
{
text: 'Accept',
onPress: async () => {
/** @type {LightningCustodianWallet} */
const fromWallet = this.state.fromWallet;
const payreq = await fromWallet.addInvoice(amount, json.makeInvoice.defaultMemo || ' ');
this.webview.postMessage(JSON.stringify({ bluewalletResponse: { paymentRequest: payreq }, id: json.id }));
},
},
],
{ cancelable: false },
);
}
if (json && json.enable) {
console.log('webln enabled');
this.setState({ weblnEnabled: true });
}
}}
onLoadStart={e => {
alreadyInjected = false;
console.log('load start');
this.setState({ pageIsLoading: true, weblnEnabled: false });
}}
onLoadEnd={e => {
console.log('load end');
this.setState({ url: e.nativeEvent.url, pageIsLoading: false });
}}
/>
);
}
};
render() {
return (
<SafeBlueArea>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<TouchableOpacity
onPress={() => {
this.webview.goBack();
}}
>
<Ionicons
name={'ios-arrow-round-back'}
size={36}
style={{
color: 'red',
backgroundColor: 'transparent',
paddingLeft: 10,
}}
/>
</TouchableOpacity>
<FormInput
inputStyle={{ color: '#0c2550', maxWidth: width - 150, fontSize: 16 }}
containerStyle={{
maxWidth: width - 150,
borderColor: '#d2d2d2',
borderWidth: 0.5,
backgroundColor: '#f5f5f5',
}}
value={this.state.url}
/>
<TouchableOpacity
onPress={() => {
processedInvoices = {};
this.setState({ url: 'https://bluewallet.io/marketplace/' });
}}
>
<Ionicons
name={'ios-home'}
size={36}
style={{
color: this.state.weblnEnabled ? 'green' : 'red',
backgroundColor: 'transparent',
}}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
let reloadUrl = this.state.url;
this.setState({ url: 'about:blank' });
processedInvoices = {};
setTimeout(() => this.setState({ url: reloadUrl }), 500);
// this.webview.reload();
}}
>
{(!this.state.pageIsLoading && (
<Ionicons
name={'ios-sync'}
size={36}
style={{
color: 'red',
backgroundColor: 'transparent',
paddingLeft: 15,
}}
/>
)) || (
<View style={{ paddingLeft: 20 }}>
<ActivityIndicator />
</View>
)}
</TouchableOpacity>
</View>
{this.renderWebView()}
</SafeBlueArea>
);
}
}
Browser.propTypes = {
navigation: PropTypes.shape({
getParam: PropTypes.func,
navigate: PropTypes.func,
}),
};

17
screen/lnd/lndCreateInvoice.js

@ -1,7 +1,7 @@
/* global alert */
import React, { Component } from 'react';
import { ActivityIndicator, View, TextInput, KeyboardAvoidingView, Keyboard, TouchableWithoutFeedback, Text } from 'react-native';
import { BlueNavigationStyle, BlueButton, BlueBitcoinAmount } from '../../BlueComponents';
import { BlueNavigationStyle, BlueButton, BlueBitcoinAmount, BlueDismissKeyboardInputAccessory } from '../../BlueComponents';
import PropTypes from 'prop-types';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
@ -35,6 +35,7 @@ export default class LNDCreateInvoice extends Component {
this.props.navigation.navigate('LNDViewInvoice', {
invoice: invoiceRequest,
fromWallet: this.state.fromWallet,
isModal: true,
});
} catch (_error) {
ReactNativeHapticFeedback.trigger('notificationError', false);
@ -46,15 +47,11 @@ export default class LNDCreateInvoice extends Component {
renderCreateButton = () => {
return (
<View style={{ paddingHorizontal: 56, paddingVertical: 16, alignContent: 'center', backgroundColor: '#FFFFFF' }}>
<View style={{ marginHorizontal: 56, marginVertical: 16, minHeight: 45, alignContent: 'center', backgroundColor: '#FFFFFF' }}>
{this.state.isLoading ? (
<ActivityIndicator />
) : (
<BlueButton
disabled={!(this.state.description.length > 0 && this.state.amount > 0)}
onPress={() => this.createInvoice()}
title={loc.send.details.create}
/>
<BlueButton disabled={!this.state.amount > 0} onPress={() => this.createInvoice()} title={loc.send.details.create} />
)}
</View>
);
@ -82,6 +79,7 @@ export default class LNDCreateInvoice extends Component {
}}
disabled={this.state.isLoading}
unit={BitcoinUnit.SATS}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
<View
style={{
@ -106,8 +104,11 @@ export default class LNDCreateInvoice extends Component {
numberOfLines={1}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
editable={!this.state.isLoading}
onSubmitEditing={Keyboard.dismiss}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
</View>
<BlueDismissKeyboardInputAccessory />
{this.renderCreateButton()}
</KeyboardAvoidingView>
</View>
@ -119,7 +120,7 @@ export default class LNDCreateInvoice extends Component {
LNDCreateInvoice.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
goBack: PropTypes.func,
navigate: PropTypes.func,
getParam: PropTypes.func,
}),

53
screen/lnd/lndViewAdditionalInvoiceInformation.js

@ -1,28 +1,29 @@
/* global alert */
import React, { Component } from 'react';
import { Animated, StyleSheet, View, TouchableOpacity, Clipboard, Share } from 'react-native';
import { BlueLoading, SafeBlueArea, BlueButton, BlueNavigationStyle, BlueText, BlueSpacing20 } from '../../BlueComponents';
import { View, Share } from 'react-native';
import {
BlueLoading,
BlueCopyTextToClipboard,
SafeBlueArea,
BlueButton,
BlueNavigationStyle,
BlueText,
BlueSpacing20,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import { QRCode } from 'react-native-custom-qr-codes';
import QRCode from 'react-native-qrcode-svg';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
const loc = require('../../loc');
export default class LNDViewAdditionalInvoiceInformation extends Component {
static navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true, () => navigation.dismiss()),
...BlueNavigationStyle(),
title: 'Additional Information',
});
state = { walletInfo: undefined };
copyToClipboard = () => {
this.setState({ addressText: loc.receive.details.copiedToClipboard }, () => {
Clipboard.setString(this.state.walletInfo.uris[0]);
setTimeout(() => this.setState({ addressText: this.state.walletInfo.uris[0] }), 1000);
});
};
async componentDidMount() {
const fromWallet = this.props.navigation.getParam('fromWallet');
try {
@ -48,21 +49,18 @@ export default class LNDViewAdditionalInvoiceInformation extends Component {
<View style={{ flex: 1, justifyContent: 'space-between', alignItems: 'center' }}>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 16 }}>
<QRCode
content={this.state.walletInfo.uris[0]}
value={this.state.walletInfo.uris[0]}
logo={require('../../img/qr-code.png')}
size={300}
logoSize={90}
color={BlueApp.settings.foregroundColor}
backgroundColor={BlueApp.settings.brandingColor}
logo={require('../../img/qr-code.png')}
logoBackgroundColor={BlueApp.settings.brandingColor}
/>
<BlueSpacing20 />
<BlueText>Open direct channel with this node:</BlueText>
<TouchableOpacity onPress={this.copyToClipboard}>
<Animated.Text style={styles.address} numberOfLines={0}>
{this.state.addressText}
</Animated.Text>
</TouchableOpacity>
<BlueCopyTextToClipboard text={this.state.walletInfo.uris[0]} />
</View>
<View style={{ marginBottom: 24 }}>
<View style={{ marginBottom: 25 }}>
<BlueButton
icon={{
name: 'share-alternative',
@ -83,19 +81,10 @@ export default class LNDViewAdditionalInvoiceInformation extends Component {
}
}
const styles = StyleSheet.create({
address: {
marginVertical: 32,
fontSize: 15,
color: '#9aa0aa',
textAlign: 'center',
},
});
LNDViewAdditionalInvoiceInformation.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
getParam: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
getParam: PropTypes.func,
dismiss: PropTypes.func,
}),
};

177
screen/lnd/lndViewInvoice.js

@ -1,23 +1,33 @@
/* global alert */
import React, { Component } from 'react';
import { Animated, StyleSheet, View, TouchableOpacity, Clipboard, Dimensions, Share } from 'react-native';
import { BlueLoading, BlueText, SafeBlueArea, BlueButton, BlueNavigationStyle, BlueSpacing20 } from '../../BlueComponents';
import { View, Dimensions, Share, ScrollView, BackHandler } from 'react-native';
import {
BlueLoading,
BlueText,
SafeBlueArea,
BlueButton,
BlueCopyTextToClipboard,
BlueNavigationStyle,
BlueSpacing20,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { Icon } from 'react-native-elements';
import QRCode from 'react-native-qrcode-svg';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
const loc = require('../../loc');
const EV = require('../../events');
const QRFast = require('react-native-qrcode');
const { width, height } = Dimensions.get('window');
export default class LNDViewInvoice extends Component {
static navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true, () => navigation.dismiss()),
title: 'Lightning Invoice',
headerLeft: null,
});
static navigationOptions = ({ navigation }) =>
navigation.getParam('isModal') === true
? {
...BlueNavigationStyle(navigation, true, () => navigation.dismiss()),
title: 'Lightning Invoice',
headerLeft: null,
}
: { ...BlueNavigationStyle(), title: 'Lightning Invoice' };
constructor(props) {
super(props);
@ -29,17 +39,21 @@ export default class LNDViewInvoice extends Component {
isLoading: typeof invoice === 'string',
addressText: typeof invoice === 'object' && invoice.hasOwnProperty('payment_request') ? invoice.payment_request : invoice,
isFetchingInvoices: true,
qrCodeHeight: height > width ? height / 2.5 : width / 2,
qrCodeHeight: height > width ? width - 20 : width / 2,
};
this.fetchInvoiceInterval = undefined;
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton.bind(this));
}
async componentDidMount() {
this.fetchInvoiceInterval = setInterval(async () => {
if (this.state.isFetchingInvoices) {
try {
const userInvoices = JSON.stringify(await this.state.fromWallet.getUserInvoices());
const updatedUserInvoice = JSON.parse(userInvoices).filter(invoice =>
const userInvoices = await this.state.fromWallet.getUserInvoices(20);
// fetching only last 20 invoices
// for invoice that was created just now - that should be enough (it is basically the last one, so limit=1 would be sufficient)
// but that might not work as intended IF user creates 21 invoices, and then tries to check the status of invoice #0, it just wont be updated
const updatedUserInvoice = userInvoices.filter(invoice =>
typeof this.state.invoice === 'object'
? invoice.payment_request === this.state.invoice.payment_request
: invoice.payment_request === this.state.invoice,
@ -53,7 +67,7 @@ export default class LNDViewInvoice extends Component {
ReactNativeHapticFeedback.trigger('notificationSuccess', false);
clearInterval(this.fetchInvoiceInterval);
this.fetchInvoiceInterval = undefined;
EV(EV.enum.TRANSACTIONS_COUNT_CHANGED);
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // remote because we want to refetch from server tx list and balance
} else {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
@ -69,11 +83,7 @@ export default class LNDViewInvoice extends Component {
}
}
} catch (error) {
clearInterval(this.fetchInvoiceInterval);
this.fetchInvoiceInterval = undefined;
console.log(error);
alert(error);
this.props.navigation.dismiss();
}
}
}, 3000);
@ -82,18 +92,17 @@ export default class LNDViewInvoice extends Component {
componentWillUnmount() {
clearInterval(this.fetchInvoiceInterval);
this.fetchInvoiceInterval = undefined;
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton.bind(this));
}
copyToClipboard = () => {
this.setState({ addressText: loc.receive.details.copiedToClipboard }, () => {
Clipboard.setString(this.state.invoice.payment_request);
setTimeout(() => this.setState({ addressText: this.state.invoice.payment_request }), 1000);
});
};
handleBackButton() {
this.props.navigation.popToTop();
return true;
}
onLayout = () => {
const { height } = Dimensions.get('window');
this.setState({ qrCodeHeight: height > width ? height / 2.5 : width / 2 });
this.setState({ qrCodeHeight: height > width ? width - 20 : width / 2 });
};
render() {
@ -164,82 +173,74 @@ export default class LNDViewInvoice extends Component {
}
}
}
// Invoice has not expired, nor has it been paid for.
return (
<SafeBlueArea>
<View
style={{
flex: 1,
alignItems: 'center',
marginTop: 8,
paddingHorizontal: 16,
justifyContent: 'space-between',
}}
onLayout={this.onLayout}
>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<QRFast
value={typeof this.state.invoice === 'object' ? invoice.payment_request : invoice}
fgColor={BlueApp.settings.brandingColor}
bgColor={BlueApp.settings.foregroundColor}
size={this.state.qrCodeHeight}
<ScrollView>
<View
style={{
flex: 1,
alignItems: 'center',
marginTop: 8,
justifyContent: 'space-between',
}}
onLayout={this.onLayout}
>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 16 }}>
<QRCode
value={typeof this.state.invoice === 'object' ? invoice.payment_request : invoice}
logo={require('../../img/qr-code.png')}
size={this.state.qrCodeHeight}
logoSize={90}
color={BlueApp.settings.foregroundColor}
logoBackgroundColor={BlueApp.settings.brandingColor}
/>
</View>
<BlueSpacing20 />
{invoice && invoice.amt && <BlueText>Please pay {invoice.amt} sats</BlueText>}
{invoice && invoice.hasOwnProperty('description') && invoice.description.length > 0 && (
<BlueText>For: {invoice.description}</BlueText>
)}
<BlueCopyTextToClipboard text={this.state.invoice.payment_request} />
<BlueButton
icon={{
name: 'share-alternative',
type: 'entypo',
color: BlueApp.settings.buttonTextColor,
}}
onPress={async () => {
Share.share({
message: 'lightning:' + invoice.payment_request,
});
}}
title={loc.receive.details.share}
/>
<BlueSpacing20 />
<BlueButton
backgroundColor="#FFFFFF"
icon={{
name: 'info',
type: 'entypo',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => this.props.navigation.navigate('LNDViewAdditionalInvoiceInformation', { fromWallet: this.state.fromWallet })}
title="Additional Information"
/>
</View>
<BlueSpacing20 />
{invoice && invoice.amt && <BlueText>Please pay {invoice.amt} sats</BlueText>}
{invoice && invoice.description && <BlueText>For: {invoice.description}</BlueText>}
<TouchableOpacity onPress={this.copyToClipboard}>
<Animated.Text style={styles.address} numberOfLines={0}>
{this.state.addressText}
</Animated.Text>
</TouchableOpacity>
<BlueButton
icon={{
name: 'share-alternative',
type: 'entypo',
color: BlueApp.settings.buttonTextColor,
}}
onPress={async () => {
Share.share({
message: 'lightning:' + invoice.payment_request,
});
}}
title={loc.receive.details.share}
/>
<BlueButton
buttonStyle={{ backgroundColor: 'white' }}
icon={{
name: 'info',
type: 'entypo',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => this.props.navigation.navigate('LNDViewAdditionalInvoiceInformation', { fromWallet: this.state.fromWallet })}
title="Additional Information"
/>
</View>
<BlueSpacing20 />
</ScrollView>
</SafeBlueArea>
);
}
}
const styles = StyleSheet.create({
address: {
marginVertical: 32,
fontSize: 15,
color: '#9aa0aa',
textAlign: 'center',
},
});
LNDViewInvoice.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
navigate: PropTypes.function,
getParam: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
navigate: PropTypes.func,
getParam: PropTypes.func,
popToTop: PropTypes.func,
}),
};

162
screen/lnd/scanLndInvoice.js

@ -1,16 +1,22 @@
/* global alert */
import React from 'react';
import { Text, Dimensions, ActivityIndicator, View, TouchableOpacity, TouchableWithoutFeedback, TextInput, Keyboard } from 'react-native';
import { Icon } from 'react-native-elements';
import { Text, ActivityIndicator, View, TouchableWithoutFeedback, Keyboard } from 'react-native';
import PropTypes from 'prop-types';
import { BlueSpacing20, BlueButton, SafeBlueArea, BlueCard, BlueNavigationStyle, BlueBitcoinAmount } from '../../BlueComponents';
import {
BlueSpacing20,
BlueButton,
SafeBlueArea,
BlueCard,
BlueNavigationStyle,
BlueAddressInput,
BlueBitcoinAmount,
} from '../../BlueComponents';
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
import { BitcoinUnit } from '../../models/bitcoinUnits';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
let EV = require('../../events');
let loc = require('../../loc');
const { width } = Dimensions.get('window');
export default class ScanLndInvoice extends React.Component {
static navigationOptions = ({ navigation }) => ({
@ -39,6 +45,7 @@ export default class ScanLndInvoice extends React.Component {
const lightningWallets = BlueApp.getWallets().filter(item => item.type === LightningCustodianWallet.type);
if (lightningWallets.length > 0) {
fromSecret = lightningWallets[0].getSecret();
console.warn('warning: using ln wallet index 0');
}
}
@ -58,32 +65,24 @@ export default class ScanLndInvoice extends React.Component {
}
async componentDidMount() {
EV(
EV.enum.CREATE_TRANSACTION_NEW_DESTINATION_ADDRESS,
data => {
this.processInvoice(data);
},
true,
);
if (this.props.navigation.state.params.uri) {
this.processTextForInvoice(this.props.navigation.getParam('uri'));
}
}
processInvoice(data) {
processInvoice = data => {
this.setState({ isLoading: true }, async () => {
if (this.ignoreRead) return;
this.ignoreRead = true;
setTimeout(() => {
this.ignoreRead = false;
}, 6000);
if (!this.state.fromWallet) {
alert('Before paying a Lightning invoice, you must first add a Lightning wallet.');
return this.props.navigation.goBack();
}
// handling BIP21 w/BOLT11 support
let ind = data.indexOf('lightning=');
if (ind !== -1) {
data = data.substring(ind + 10).split('&')[0];
}
data = data.replace('LIGHTNING:', '').replace('lightning:', '');
console.log(data);
@ -111,11 +110,12 @@ export default class ScanLndInvoice extends React.Component {
isLoading: false,
});
} catch (Err) {
Keyboard.dismiss();
this.setState({ isLoading: false });
alert(Err.message);
}
});
}
};
async pay() {
if (!this.state.hasOwnProperty('decoded')) {
@ -138,29 +138,26 @@ export default class ScanLndInvoice extends React.Component {
return alert('Invoice expired');
}
const currentUserInvoices = await fromWallet.getUserInvoices();
const currentUserInvoices = fromWallet.user_invoices_raw; // not fetching invoices, as we assume they were loaded previously
if (currentUserInvoices.some(invoice => invoice.payment_hash === decoded.payment_hash)) {
this.setState({ isLoading: false });
return alert(loc.lnd.sameWalletAsInvoiceError);
}
let start = +new Date();
let end;
try {
await fromWallet.payInvoice(this.state.invoice, this.state.decoded.num_satoshis);
end = +new Date();
} catch (Err) {
console.log(Err.message);
this.setState({ isLoading: false });
this.props.navigation.goBack();
return alert('Error');
return alert(Err.message);
}
console.log('payInvoice took', (end - start) / 1000, 'sec');
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
alert('Success');
this.props.navigation.goBack();
this.props.navigation.navigate('Success', {
amount: this.state.decoded.num_satoshis,
amountUnit: BitcoinUnit.SATS,
invoiceDescription: this.state.decoded.description,
});
},
);
}
@ -205,53 +202,16 @@ export default class ScanLndInvoice extends React.Component {
/>
<BlueSpacing20 />
<BlueCard>
<View
style={{
flexDirection: 'row',
borderColor: '#d2d2d2',
borderBottomColor: '#d2d2d2',
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: '#f5f5f5',
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 8,
borderRadius: 4,
<BlueAddressInput
onChangeText={text => {
this.setState({ destination: text });
this.processTextForInvoice(text);
}}
>
<TextInput
onChangeText={text => {
this.setState({ destination: text });
this.processTextForInvoice(text);
}}
placeholder={loc.wallets.details.destination}
numberOfLines={1}
value={this.state.destination}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33, height: 33 }}
editable={!this.state.isLoading}
/>
<TouchableOpacity
disabled={this.state.isLoading}
onPress={() => this.props.navigation.navigate('ScanQrAddress')}
style={{
width: 75,
height: 36,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#bebebe',
borderRadius: 4,
paddingVertical: 4,
paddingHorizontal: 8,
marginHorizontal: 4,
}}
>
<Icon name="qrcode" size={22} type="font-awesome" color="#FFFFFF" />
<Text style={{ color: '#FFFFFF' }}>{loc.send.details.scan}</Text>
</TouchableOpacity>
</View>
onBarScanned={this.processInvoice}
address={this.state.destination}
isLoading={this.state.isLoading}
placeholder={loc.lnd.placeholder}
/>
<View
style={{
flexDirection: 'row',
@ -268,27 +228,29 @@ export default class ScanLndInvoice extends React.Component {
{this.state.expiresIn !== undefined && (
<Text style={{ color: '#81868e', fontSize: 12, left: 20, top: 10 }}>Expires in: {this.state.expiresIn}</Text>
)}
<BlueSpacing20 />
<BlueSpacing20 />
<BlueCard>
{this.state.isLoading ? (
<View>
<ActivityIndicator />
</View>
) : (
<BlueButton
icon={{
name: 'bolt',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
title={'Pay'}
onPress={() => {
this.pay();
}}
disabled={this.shouldDisablePayButton()}
/>
)}
</BlueCard>
</BlueCard>
<BlueSpacing20 />
{this.state.isLoading ? (
<View>
<ActivityIndicator />
</View>
) : (
<BlueButton
icon={{
name: 'bolt',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
title={'Pay'}
buttonStyle={{ width: 150, left: (width - 150) / 2 - 20 }}
onPress={() => {
this.pay();
}}
disabled={this.shouldDisablePayButton()}
/>
)}
</SafeBlueArea>
</TouchableWithoutFeedback>
);
@ -297,10 +259,10 @@ export default class ScanLndInvoice extends React.Component {
ScanLndInvoice.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
navigate: PropTypes.function,
getParam: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
navigate: PropTypes.func,
getParam: PropTypes.func,
dismiss: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
uri: PropTypes.string,

54
screen/receive/details.js

@ -1,8 +1,16 @@
import React, { Component } from 'react';
import { Animated, StyleSheet, View, TouchableOpacity, Clipboard, Share } from 'react-native';
import { QRCode } from 'react-native-custom-qr-codes';
import { View, Share } from 'react-native';
import QRCode from 'react-native-qrcode-svg';
import bip21 from 'bip21';
import { BlueLoading, SafeBlueArea, BlueButton, BlueButtonLink, BlueNavigationStyle, is } from '../../BlueComponents';
import {
BlueLoading,
SafeBlueArea,
BlueCopyTextToClipboard,
BlueButton,
BlueButtonLink,
BlueNavigationStyle,
is,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
@ -28,10 +36,10 @@ export default class ReceiveDetails extends Component {
addressText: '',
};
// EV(EV.enum.RECEIVE_ADDRESS_CHANGED, this.refreshFunction.bind(this));
// EV(EV.enum.RECEIVE_ADDRESS_CHANGED, this.redrawScreen.bind(this));
}
/* refreshFunction(newAddress) {
/* redrawScreen(newAddress) {
console.log('newAddress =', newAddress);
this.setState({
address: newAddress,
@ -70,15 +78,7 @@ export default class ReceiveDetails extends Component {
}
}
copyToClipboard = () => {
this.setState({ addressText: loc.receive.details.copiedToClipboard }, () => {
Clipboard.setString(this.state.address);
setTimeout(() => this.setState({ addressText: this.state.address }), 1000);
});
};
render() {
console.log('render() receive/details, address,secret=', this.state.address, ',', this.state.secret);
if (this.state.isLoading) {
return <BlueLoading />;
}
@ -88,19 +88,16 @@ export default class ReceiveDetails extends Component {
<View style={{ flex: 1, justifyContent: 'space-between', alignItems: 'center' }}>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 16 }}>
<QRCode
content={bip21.encode(this.state.address)}
value={bip21.encode(this.state.address)}
logo={require('../../img/qr-code.png')}
size={(is.ipad() && 300) || 300}
logoSize={90}
color={BlueApp.settings.foregroundColor}
backgroundColor={BlueApp.settings.brandingColor}
logo={require('../../img/qr-code.png')}
logoBackgroundColor={BlueApp.settings.brandingColor}
/>
<TouchableOpacity onPress={this.copyToClipboard}>
<Animated.Text style={styles.address} numberOfLines={0}>
{this.state.addressText}
</Animated.Text>
</TouchableOpacity>
<BlueCopyTextToClipboard text={this.state.addressText} />
</View>
<View style={{ marginBottom: 24, alignItems: 'center' }}>
<View style={{ flex: 0.2, marginBottom: 24, alignItems: 'center' }}>
<BlueButtonLink
title={loc.receive.details.setAmount}
onPress={() => {
@ -129,19 +126,10 @@ export default class ReceiveDetails extends Component {
}
}
const styles = StyleSheet.create({
address: {
marginVertical: 32,
fontSize: 15,
color: '#9aa0aa',
textAlign: 'center',
},
});
ReceiveDetails.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
navigate: PropTypes.function,
goBack: PropTypes.func,
navigate: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
address: PropTypes.string,

129
screen/receive/receiveAmount.js

@ -1,21 +1,16 @@
import React, { Component } from 'react';
import {
StyleSheet,
View,
Share,
TextInput,
KeyboardAvoidingView,
Clipboard,
Animated,
TouchableOpacity,
Platform,
Dimensions,
ScrollView,
} from 'react-native';
import { QRCode as QRSlow } from 'react-native-custom-qr-codes';
import QRFast from 'react-native-qrcode';
import { View, Share, TextInput, KeyboardAvoidingView, Dimensions, ScrollView } from 'react-native';
import QRCode from 'react-native-qrcode-svg';
import bip21 from 'bip21';
import { SafeBlueArea, BlueButton, BlueNavigationStyle, BlueBitcoinAmount, BlueText } from '../../BlueComponents';
import {
SafeBlueArea,
BlueCard,
BlueButton,
BlueNavigationStyle,
BlueBitcoinAmount,
BlueText,
BlueCopyTextToClipboard,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
@ -52,13 +47,6 @@ export default class ReceiveAmount extends Component {
};
}
copyToClipboard = () => {
this.setState({ addressText: loc.receive.details.copiedToClipboard }, () => {
Clipboard.setString(this.state.bip21);
setTimeout(() => this.setState({ addressText: this.state.address }), 1000);
});
};
determineSize = () => {
if (width > 312) {
return width - 48;
@ -94,15 +82,17 @@ export default class ReceiveAmount extends Component {
editable={!this.state.isLoading}
/>
</View>
<BlueButton
title="Create"
onPress={() => {
this.setState({
amountSet: true,
bip21: bip21.encode(this.state.address, { amount: this.state.amount, label: this.state.label }),
});
}}
/>
<BlueCard>
<BlueButton
title={loc.receive.details.create}
onPress={() => {
this.setState({
amountSet: true,
bip21: bip21.encode(this.state.address, { amount: this.state.amount, label: this.state.label }),
});
}}
/>
</BlueCard>
</View>
);
}
@ -114,30 +104,18 @@ export default class ReceiveAmount extends Component {
{this.state.label}
</BlueText>
<View style={{ justifyContent: 'center', alignItems: 'center', paddingHorizontal: 16 }}>
{Platform.OS === 'ios' || this.state.bip21.length < 54 ? (
<QRSlow
content={this.state.bip21}
size={this.determineSize()}
color={BlueApp.settings.foregroundColor}
backgroundColor={BlueApp.settings.brandingColor}
logo={require('../../img/qr-code.png')}
ecl={'Q'}
/>
) : (
<QRFast
value={this.state.bip21}
size={this.determineSize()}
fgColor={BlueApp.settings.brandingColor}
bgColor={BlueApp.settings.foregroundColor}
/>
)}
<QRCode
value={this.state.bip21}
logo={require('../../img/qr-code.png')}
size={this.determineSize()}
logoSize={90}
color={BlueApp.settings.foregroundColor}
logoBackgroundColor={BlueApp.settings.brandingColor}
ecl={'Q'}
/>
</View>
<View style={{ marginBottom: 24, alignItems: 'center', justifyContent: 'space-between' }}>
<TouchableOpacity onPress={this.copyToClipboard}>
<Animated.Text style={styles.address} numberOfLines={0}>
{this.state.bip21}
</Animated.Text>
</TouchableOpacity>
<View style={{ alignItems: 'center', justifyContent: 'space-between' }}>
<BlueCopyTextToClipboard text={this.state.bip21} />
</View>
</View>
);
@ -157,23 +135,21 @@ export default class ReceiveAmount extends Component {
{this.state.amountSet ? this.renderWithSetAmount() : this.renderDefault()}
</KeyboardAvoidingView>
{this.state.amountSet && (
<BlueButton
buttonStyle={{
alignSelf: 'center',
marginBottom: 24,
}}
icon={{
name: 'share-alternative',
type: 'entypo',
color: BlueApp.settings.buttonTextColor,
}}
onPress={async () => {
Share.share({
message: this.state.bip21,
});
}}
title={loc.receive.details.share}
/>
<BlueCard>
<BlueButton
icon={{
name: 'share-alternative',
type: 'entypo',
color: BlueApp.settings.buttonTextColor,
}}
onPress={async () => {
Share.share({
message: this.state.bip21,
});
}}
title={loc.receive.details.share}
/>
</BlueCard>
)}
</View>
</ScrollView>
@ -181,12 +157,3 @@ export default class ReceiveAmount extends Component {
);
}
}
const styles = StyleSheet.create({
address: {
marginVertical: 32,
fontSize: 15,
color: '#9aa0aa',
textAlign: 'center',
},
});

17
screen/selftest.js

@ -5,6 +5,8 @@ import PropTypes from 'prop-types';
import { SegwitP2SHWallet, LegacyWallet, HDSegwitP2SHWallet } from '../class';
let BigNumber = require('bignumber.js');
let encryption = require('../encryption');
let bitcoin = require('bitcoinjs-lib');
let BlueElectrum = require('../BlueElectrum');
export default class Selftest extends Component {
static navigationOptions = () => ({
@ -42,6 +44,20 @@ export default class Selftest extends Component {
//
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
let addr4elect = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
let electrumBalance = await BlueElectrum.getBalanceByAddress(addr4elect);
if (electrumBalance.confirmed !== 51432)
throw new Error('BlueElectrum getBalanceByAddress failure, got ' + JSON.stringify(electrumBalance));
let electrumTxs = await BlueElectrum.getTransactionsByAddress(addr4elect);
if (electrumTxs.length !== 1) throw new Error('BlueElectrum getTransactionsByAddress failure, got ' + JSON.stringify(electrumTxs));
} else {
console.warn('skipping RN-specific test');
}
//
let l = new LegacyWallet();
l.setSecret('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct');
if (l.getAddress() !== '19AAjaTUbRjQCMuVczepkoPswiZRhjtg31') {
@ -158,7 +174,6 @@ export default class Selftest extends Component {
];
let tx = l.createTx(utxo, 0.001, 0.0001, '1QHf8Gp3wfmFiSdEX4FtrssCGR68diN1cj');
let bitcoin = require('bitcoinjs-lib');
let txDecoded = bitcoin.Transaction.fromHex(tx);
let txid = txDecoded.getId();

18
screen/send/confirm.js

@ -105,15 +105,7 @@ export default class Confirm extends Component {
<Text style={styles.transactionDetailsTitle}>{loc.send.create.to}</Text>
<Text style={styles.transactionDetailsSubtitle}>{this.state.address}</Text>
<BlueSpacing40 />
{this.state.isLoading ? (
<ActivityIndicator />
) : (
<BlueButton
onPress={() => this.broadcast()}
title={loc.send.confirm.sendNow}
style={{ maxWidth: 263, paddingHorizontal: 56 }}
/>
)}
{this.state.isLoading ? <ActivityIndicator /> : <BlueButton onPress={() => this.broadcast()} title={loc.send.confirm.sendNow} />}
<TouchableOpacity
style={{ marginVertical: 24 }}
onPress={() =>
@ -154,10 +146,10 @@ const styles = StyleSheet.create({
Confirm.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
getParam: PropTypes.function,
navigate: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
getParam: PropTypes.func,
navigate: PropTypes.func,
dismiss: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
amount: PropTypes.string,

8
screen/send/create.js

@ -108,10 +108,10 @@ const styles = StyleSheet.create({
SendCreate.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
getParam: PropTypes.function,
navigate: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
getParam: PropTypes.func,
navigate: PropTypes.func,
dismiss: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
amount: PropTypes.string,

313
screen/send/details.js

@ -4,6 +4,7 @@ import {
ActivityIndicator,
View,
TextInput,
StatusBar,
TouchableOpacity,
KeyboardAvoidingView,
Keyboard,
@ -11,19 +12,26 @@ import {
StyleSheet,
Platform,
Slider,
AsyncStorage,
Text,
} from 'react-native';
import { Icon } from 'react-native-elements';
import { BlueNavigationStyle, BlueButton, BlueBitcoinAmount } from '../../BlueComponents';
import {
BlueNavigationStyle,
BlueButton,
BlueBitcoinAmount,
BlueAddressInput,
BlueDismissKeyboardInputAccessory,
BlueLoading,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import Modal from 'react-native-modal';
import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees';
import BitcoinBIP70TransactionDecode from '../../bip70/bip70';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { HDLegacyP2PKHWallet, HDSegwitP2SHWallet } from '../../class';
import { HDLegacyP2PKHWallet, HDSegwitP2SHWallet, LightningCustodianWallet } from '../../class';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
const bip21 = require('bip21');
let EV = require('../../events');
let BigNumber = require('bignumber.js');
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
@ -38,6 +46,8 @@ export default class SendDetails extends Component {
title: loc.send.header,
});
state = { isLoading: true, fromWallet: undefined };
constructor(props) {
super(props);
console.log('props.navigation.state.params=', props.navigation.state.params);
@ -51,59 +61,79 @@ export default class SendDetails extends Component {
if (props.navigation.state.params) fromSecret = props.navigation.state.params.fromSecret;
let fromWallet = null;
const wallets = BlueApp.getWallets();
const wallets = BlueApp.getWallets().filter(wallet => wallet.type !== LightningCustodianWallet.type);
for (let w of wallets) {
if (w.getSecret() === fromSecret) {
fromWallet = w;
break;
if (wallets.length === 0) {
alert('Before creating a transaction, you must first add a Bitcoin wallet.');
return props.navigation.goBack(null);
} else {
if (!fromWallet && wallets.length > 0) {
fromWallet = wallets[0];
fromAddress = fromWallet.getAddress();
fromSecret = fromWallet.getSecret();
}
if (fromWallet === null) return props.navigation.goBack(null);
for (let w of wallets) {
if (w.getSecret() === fromSecret) {
fromWallet = w;
break;
}
if (w.getAddress() === fromAddress) {
fromWallet = w;
if (w.getAddress() === fromAddress) {
fromWallet = w;
}
}
}
// fallback to first wallet if it exists
if (!fromWallet && wallets[0]) fromWallet = wallets[0];
this.state = {
isFeeSelectionModalVisible: false,
fromAddress,
fromWallet,
fromSecret,
isLoading: true,
address,
memo,
fee: 1,
networkTransactionFees: new NetworkTransactionFee(1, 1, 1),
feeSliderValue: 1,
bip70TransactionExpiration: null,
};
this.state = {
isFeeSelectionModalVisible: false,
fromAddress,
fromWallet,
fromSecret,
address,
memo,
fee: 1,
networkTransactionFees: new NetworkTransactionFee(1, 1, 1),
feeSliderValue: 1,
bip70TransactionExpiration: null,
renderWalletSelectionButtonHidden: false,
};
}
}
async componentDidMount() {
EV(EV.enum.CREATE_TRANSACTION_NEW_DESTINATION_ADDRESS, data => {
this.setState(
{ isLoading: false },
() => {
if (btcAddressRx.test(data) || data.indexOf('bc1') === 0) {
/**
* TODO: refactor this mess, get rid of regexp, use https://github.com/bitcoinjs/bitcoinjs-lib/issues/890 etc etc
*
* @param data {String} Can be address or `bitcoin:xxxxxxx` uri scheme, or invalid garbage
*/
processAddressData = data => {
this.setState(
{ isLoading: true },
() => {
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(data)) {
this.processBIP70Invoice(data);
} else {
const dataWithoutSchema = data.replace('bitcoin:', '');
if (btcAddressRx.test(dataWithoutSchema) || (dataWithoutSchema.indexOf('bc1') === 0 && dataWithoutSchema.indexOf('?') === -1)) {
this.setState({
address: data,
address: dataWithoutSchema,
bip70TransactionExpiration: null,
isLoading: false,
});
} else {
let address, options;
try {
if (!data.toLowerCase().startsWith('bitcoin:')) {
data = `bitcoin:${data}`;
}
const decoded = bip21.decode(data);
address = decoded.address;
options = decoded.options;
} catch (Err) {
console.log(Err);
} catch (error) {
console.log(error);
this.setState({ isLoading: false });
}
console.log(options);
if (btcAddressRx.test(address)) {
if (btcAddressRx.test(address) || address.indexOf('bc1') === 0) {
this.setState({
address,
amount: options.amount,
@ -111,28 +141,37 @@ export default class SendDetails extends Component {
bip70TransactionExpiration: null,
isLoading: false,
});
} else if (BitcoinBIP70TransactionDecode.matchesPaymentURL(data)) {
this.processBIP70Invoice(data);
}
}
},
true,
);
});
let recommendedFees = await NetworkTransactionFees.recommendedFees().catch(response => {
this.setState({
fee: response.halfHourFee,
networkTransactionFees: response,
feeSliderValue: response.halfHourFee,
isLoading: false,
});
});
if (recommendedFees) {
}
},
true,
);
};
async componentDidMount() {
StatusBar.setBarStyle('dark-content');
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
try {
const cachedNetworkTransactionFees = JSON.parse(await AsyncStorage.getItem(NetworkTransactionFee.StorageKey));
if (cachedNetworkTransactionFees && cachedNetworkTransactionFees.hasOwnProperty('halfHourFee')) {
this.setState({
fee: cachedNetworkTransactionFees.halfHourFee,
networkTransactionFees: cachedNetworkTransactionFees,
feeSliderValue: cachedNetworkTransactionFees.halfHourFee,
});
}
} catch (_) {}
let recommendedFees = await NetworkTransactionFees.recommendedFees();
if (recommendedFees && recommendedFees.hasOwnProperty('halfHourFee')) {
await AsyncStorage.setItem(NetworkTransactionFee.StorageKey, JSON.stringify(recommendedFees));
this.setState({
fee: recommendedFees.halfHourFee,
networkTransactionFees: recommendedFees,
feeSliderValue: recommendedFees.halfHourFee,
isLoading: false,
});
if (this.props.navigation.state.params.uri) {
@ -140,28 +179,54 @@ export default class SendDetails extends Component {
this.processBIP70Invoice(this.props.navigation.state.params.uri);
} else {
try {
let amount = '';
let parsedBitcoinUri = null;
let address = '';
let memo = '';
parsedBitcoinUri = bip21.decode(this.props.navigation.state.params.uri);
address = parsedBitcoinUri.hasOwnProperty('address') ? parsedBitcoinUri.address : address;
if (parsedBitcoinUri.hasOwnProperty('options')) {
if (parsedBitcoinUri.options.hasOwnProperty('amount')) {
amount = parsedBitcoinUri.options.amount.toString();
}
if (parsedBitcoinUri.options.hasOwnProperty('label')) {
memo = parsedBitcoinUri.options.label || memo;
}
}
this.setState({ address, amount, memo });
const { address, amount, memo } = this.decodeBitcoinUri(this.props.navigation.getParam('uri'));
this.setState({ address, amount, memo, isLoading: false });
} catch (error) {
console.log(error);
this.setState({ isLoading: false });
alert('Error: Unable to decode Bitcoin address');
}
}
} else {
this.setState({ isLoading: false });
}
} else {
this.setState({ isLoading: false });
}
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
_keyboardDidShow = () => {
this.setState({ renderWalletSelectionButtonHidden: true });
};
_keyboardDidHide = () => {
this.setState({ renderWalletSelectionButtonHidden: false });
};
decodeBitcoinUri(uri) {
try {
let amount = '';
let parsedBitcoinUri = null;
let address = '';
let memo = '';
parsedBitcoinUri = bip21.decode(uri);
address = parsedBitcoinUri.hasOwnProperty('address') ? parsedBitcoinUri.address : address;
if (parsedBitcoinUri.hasOwnProperty('options')) {
if (parsedBitcoinUri.options.hasOwnProperty('amount')) {
amount = parsedBitcoinUri.options.amount.toString();
}
if (parsedBitcoinUri.options.hasOwnProperty('label')) {
memo = parsedBitcoinUri.options.label || memo;
}
}
return { address, amount, memo };
} catch (_) {
return undefined;
}
}
@ -242,6 +307,7 @@ export default class SendDetails extends Component {
}
async createTransaction() {
Keyboard.dismiss();
this.setState({ isLoading: true });
let error = false;
let requestedSatPerByte = this.state.fee.toString().replace(/\D/g, '');
@ -367,9 +433,9 @@ export default class SendDetails extends Component {
}
onWalletSelect = wallet => {
this.setState({ fromAddress: wallet.getAddress(), fromSecret: wallet.getSecret(), fromWallet: wallet }, () =>
this.props.navigation.goBack(null),
);
this.setState({ fromAddress: wallet.getAddress(), fromSecret: wallet.getSecret(), fromWallet: wallet }, () => {
this.props.navigation.pop();
});
};
renderFeeSelectionModal = () => {
@ -377,7 +443,13 @@ export default class SendDetails extends Component {
<Modal
isVisible={this.state.isFeeSelectionModalVisible}
style={styles.bottomModal}
onBackdropPress={() => this.setState({ isFeeSelectionModalVisible: false })}
onBackdropPress={() => {
if (this.state.fee < 1 || this.state.feeSliderValue < 1) {
this.setState({ fee: Number(1), feeSliderValue: Number(1) });
}
Keyboard.dismiss();
this.setState({ isFeeSelectionModalVisible: false });
}}
>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null}>
<View style={styles.modalContent}>
@ -388,18 +460,21 @@ export default class SendDetails extends Component {
this.textInput = ref;
}}
value={this.state.fee.toString()}
onEndEditing={() => {
if (this.state.fee < 1 || this.state.feeSliderValue < 1) {
this.setState({ fee: Number(1), feeSliderValue: Number(1) });
}
}}
onChangeText={value => {
let newValue = value.replace(/\D/g, '');
if (newValue.length === 0) {
newValue = 1;
}
this.setState({ fee: newValue, feeSliderValue: newValue });
this.setState({ fee: Number(newValue), feeSliderValue: Number(newValue) });
}}
maxLength={9}
editable={!this.state.isLoading}
placeholderTextColor="#37c0a1"
placeholder={this.state.networkTransactionFees.halfHourFee.toString()}
style={{ fontWeight: '600', color: '#37c0a1', marginBottom: 0, marginRight: 4, textAlign: 'right', fontSize: 36 }}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
<Text
style={{
@ -451,6 +526,7 @@ export default class SendDetails extends Component {
};
renderWalletSelectionButton = () => {
if (this.state.renderWalletSelectionButtonHidden) return;
return (
<View style={{ marginBottom: 24, alignItems: 'center' }}>
{!this.state.isLoading && (
@ -478,14 +554,13 @@ export default class SendDetails extends Component {
};
render() {
if (!this.state.fromWallet.getAddress) {
if (this.state.isLoading || typeof this.state.fromWallet === 'undefined') {
return (
<View style={{ flex: 1, paddingTop: 20 }}>
<Text>System error: Source wallet not found (this should never happen)</Text>
<BlueLoading />
</View>
);
}
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<View style={{ flex: 1, justifyContent: 'space-between' }}>
@ -495,57 +570,30 @@ export default class SendDetails extends Component {
isLoading={this.state.isLoading}
amount={this.state.amount}
onChangeText={text => this.setState({ amount: text })}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
<View
style={{
flexDirection: 'row',
borderColor: '#d2d2d2',
borderBottomColor: '#d2d2d2',
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: '#f5f5f5',
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 8,
borderRadius: 4,
}}
>
<TextInput
onChangeText={text => {
if (!this.processBIP70Invoice(text)) {
this.setState({ address: text.replace(' ', ''), isLoading: false, bip70TransactionExpiration: null });
} else {
this.setState({ address: text.replace(' ', ''), isLoading: false, bip70TransactionExpiration: null });
<BlueAddressInput
onChangeText={text => {
if (!this.processBIP70Invoice(text)) {
this.setState({
address: text.trim().replace('bitcoin:', ''),
isLoading: false,
bip70TransactionExpiration: null,
});
} else {
try {
const { address, amount, memo } = this.decodeBitcoinUri(text);
this.setState({ address, amount, memo, isLoading: false, bip70TransactionExpiration: null });
} catch (_) {
this.setState({ address: text.trim(), isLoading: false, bip70TransactionExpiration: null });
}
}}
placeholder={loc.send.details.address}
numberOfLines={1}
value={this.state.address}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
editable={!this.state.isLoading}
/>
<TouchableOpacity
disabled={this.state.isLoading}
onPress={() => this.props.navigation.navigate('ScanQrAddress')}
style={{
width: 75,
height: 36,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#bebebe',
borderRadius: 4,
paddingVertical: 4,
paddingHorizontal: 8,
marginHorizontal: 4,
}}
>
<Icon name="qrcode" size={22} type="font-awesome" color="#FFFFFF" />
<Text style={{ color: '#FFFFFF' }}>{loc.send.details.scan}</Text>
</TouchableOpacity>
</View>
}
}}
onBarScanned={this.processAddressData}
address={this.state.address}
isLoading={this.state.isLoading}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
<View
hide={!this.state.showMemoRow}
style={{
@ -570,6 +618,8 @@ export default class SendDetails extends Component {
numberOfLines={1}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
editable={!this.state.isLoading}
onSubmitEditing={Keyboard.dismiss}
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
</View>
<TouchableOpacity
@ -598,6 +648,7 @@ export default class SendDetails extends Component {
{this.renderFeeSelectionModal()}
</KeyboardAvoidingView>
</View>
<BlueDismissKeyboardInputAccessory />
{this.renderWalletSelectionButton()}
</View>
</TouchableWithoutFeedback>
@ -634,8 +685,10 @@ const styles = StyleSheet.create({
SendDetails.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
pop: PropTypes.func,
goBack: PropTypes.func,
navigate: PropTypes.func,
getParam: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
address: PropTypes.string,

29
screen/send/scanQrAddress.js

@ -1,11 +1,9 @@
/* global alert */
import React from 'react';
import { ActivityIndicator, Image, View, TouchableOpacity } from 'react-native';
import PropTypes from 'prop-types';
import Camera from 'react-native-camera';
import Permissions from 'react-native-permissions';
import { SafeBlueArea } from '../../BlueComponents';
let EV = require('../../events');
export default class CameraExample extends React.Component {
static navigationOptions = {
@ -17,18 +15,16 @@ export default class CameraExample extends React.Component {
hasCameraPermission: null,
};
async onBarCodeScanned(ret) {
if (this.ignoreRead) return;
this.ignoreRead = true;
setTimeout(() => {
this.ignoreRead = false;
}, 2000);
this.props.navigation.goBack();
EV(EV.enum.CREATE_TRANSACTION_NEW_DESTINATION_ADDRESS, ret.data);
onBarCodeScanned(ret) {
if (this.state.isLoading) return;
this.setState({ isLoading: true }, () => {
const onBarScanned = this.props.navigation.getParam('onBarScanned');
this.props.navigation.goBack();
onBarScanned(ret.data);
});
} // end
async componentDidMount() {
componentDidMount() {
Permissions.request('camera').then(response => {
// Response is one of: 'authorized', 'denied', 'restricted', or 'undetermined'
this.setState({ hasCameraPermission: response === 'authorized' });
@ -48,13 +44,11 @@ export default class CameraExample extends React.Component {
if (hasCameraPermission === null) {
return <View />;
} else if (hasCameraPermission === false) {
alert('BlueWallet does not have permission to use your camera.');
this.props.navigation.goBack(null);
return <View />;
} else {
return (
<SafeBlueArea style={{ flex: 1 }}>
<Camera style={{ flex: 1 }} onBarCodeRead={ret => this.onBarCodeScanned(ret)}>
<Camera style={{ flex: 1, justifyContent: 'space-between' }} onBarCodeRead={ret => this.onBarCodeScanned(ret)}>
<TouchableOpacity
style={{ width: 40, height: 80, padding: 14, marginTop: 32 }}
onPress={() => this.props.navigation.goBack(null)}
@ -70,7 +64,8 @@ export default class CameraExample extends React.Component {
CameraExample.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
dismiss: PropTypes.func,
getParam: PropTypes.func,
}),
};

58
screen/send/success.js

@ -18,7 +18,9 @@ export default class Success extends Component {
this.state = {
amount: props.navigation.getParam('amount'),
fee: props.navigation.getParam('fee'),
fee: props.navigation.getParam('fee') || 0,
amountUnit: props.navigation.getParam('amountUnit') || BitcoinUnit.BTC,
invoiceDescription: props.navigation.getParam('invoiceDescription') || '',
};
}
@ -51,21 +53,38 @@ export default class Success extends Component {
alignSelf: 'flex-end',
}}
>
{' ' + BitcoinUnit.BTC}
{' ' + this.state.amountUnit}
</Text>
</View>
<Text
style={{
color: '#37c0a1',
fontSize: 14,
marginHorizontal: 4,
paddingBottom: 6,
fontWeight: '500',
alignSelf: 'center',
}}
>
{loc.send.create.fee}: {loc.formatBalance(this.state.fee, BitcoinUnit.SATS)}
</Text>
{this.state.fee > 0 && (
<Text
style={{
color: '#37c0a1',
fontSize: 14,
marginHorizontal: 4,
paddingBottom: 6,
fontWeight: '500',
alignSelf: 'center',
}}
>
{loc.send.create.fee}: {loc.formatBalance(this.state.fee, BitcoinUnit.SATS)}
</Text>
)}
{this.state.fee <= 0 && (
<Text
numberOfLines={0}
style={{
color: '#37c0a1',
fontSize: 14,
marginHorizontal: 4,
paddingBottom: 6,
fontWeight: '500',
alignSelf: 'center',
}}
>
{this.state.invoiceDescription}
</Text>
)}
</BlueCard>
<View
style={{
@ -87,7 +106,6 @@ export default class Success extends Component {
this.props.navigation.dismiss();
}}
title={loc.send.success.done}
style={{ maxWidth: 263, paddingHorizontal: 56 }}
/>
</BlueCard>
</SafeBlueArea>
@ -97,13 +115,13 @@ export default class Success extends Component {
Success.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
getParam: PropTypes.function,
navigate: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
getParam: PropTypes.func,
navigate: PropTypes.func,
dismiss: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
amount: PropTypes.string,
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
fee: PropTypes.number,
}),
}),

128
screen/settings/about.js

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { ScrollView, Linking, Dimensions, Platform } from 'react-native';
import { ScrollView, Linking, Dimensions } from 'react-native';
import {
BlueTextCentered,
BlueLoading,
@ -12,6 +12,7 @@ import {
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import DeviceInfo from 'react-native-device-info';
import Rate, { AndroidMarket } from 'react-native-rate';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
const { width, height } = Dimensions.get('window');
@ -50,65 +51,71 @@ export default class About extends Component {
<BlueTextCentered h4>Always backup your keys</BlueTextCentered>
<BlueSpacing20 />
</BlueCard>
<BlueButton
icon={{
name: 'mark-github',
type: 'octicon',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => {
Linking.openURL('https://github.com/BlueWallet/BlueWallet');
}}
title="github.com/BlueWallet/BlueWallet"
/>
<BlueSpacing20 />
<BlueButton
icon={{
name: 'mark-github',
type: 'octicon',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => {
Linking.openURL('https://github.com/BlueWallet/BlueWallet');
}}
title="github.com/BlueWallet/BlueWallet"
/>
<BlueSpacing20 />
<BlueButton
icon={{
name: 'twitter',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => {
Linking.openURL('https://twitter.com/bluewalletio');
}}
title="Follow us on Twitter"
/>
<BlueSpacing20 />
<BlueButton
icon={{
name: 'twitter',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => {
Linking.openURL('https://twitter.com/bluewalletio');
}}
title="Follow us on Twitter"
/>
<BlueSpacing20 />
<BlueButton
icon={{
name: 'telegram',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => {
Linking.openURL('https://t.me/bluewallet');
}}
title="Join Telegram chat"
/>
<BlueSpacing20 />
<BlueButton
icon={{
name: 'telegram',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => {
Linking.openURL('https://t.me/bluewallet');
}}
title="Join Telegram chat"
/>
<BlueSpacing20 />
<BlueButton
icon={{
name: 'thumbsup',
type: 'octicon',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => {
if (Platform.OS === 'ios') {
Linking.openURL('https://itunes.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040?l=ru&ls=1&mt=8');
} else {
Linking.openURL('https://play.google.com/store/apps/details?id=io.bluewallet.bluewallet');
}
}}
title="Leave us a review on Appstore"
/>
<BlueButton
icon={{
name: 'thumbsup',
type: 'octicon',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => {
let options = {
AppleAppID: '1376878040',
GooglePackageName: 'io.bluewallet.bluewallet',
preferredAndroidMarket: AndroidMarket.Google,
preferInApp: true,
openAppStoreIfInAppFails: true,
fallbackPlatformURL: 'https://bluewallet.io',
};
Rate.rate(options, success => {
if (success) {
console.log('User Rated.');
}
});
}}
title="Rate BlueWallet"
/>
<BlueSpacing20 />
<BlueCard>
<BlueSpacing20 />
<BlueText h3>Built with awesome:</BlueText>
<BlueSpacing20 />
<BlueText h4>* React Native</BlueText>
@ -120,6 +127,14 @@ export default class About extends Component {
<BlueText h4>* bignumber.js</BlueText>
<BlueSpacing20 />
<BlueButton
onPress={() => {
this.props.navigation.navigate('ReleaseNotes');
}}
title="Release notes"
/>
<BlueSpacing20 />
<BlueButton
onPress={() => {
this.props.navigation.navigate('Selftest');
@ -128,8 +143,9 @@ export default class About extends Component {
/>
<BlueTextCentered />
<BlueTextCentered>
{DeviceInfo.getApplicationName()} ver {DeviceInfo.getVersion()} (build number {DeviceInfo.getBuildNumber()})
{DeviceInfo.getApplicationName()} ver {DeviceInfo.getVersion()} (build {DeviceInfo.getBuildNumber()})
</BlueTextCentered>
<BlueTextCentered>{new Date(DeviceInfo.getBuildNumber() * 1000).toGMTString()}</BlueTextCentered>
<BlueTextCentered>{DeviceInfo.getBundleId()}</BlueTextCentered>
<BlueTextCentered>
w, h = {width}, {height}

2
screen/settings/currency.js

@ -42,7 +42,7 @@ export default class Currency extends Component {
}}
>
<BlueListItem
title={item.symbol + ' ' + item.endPointKey}
title={item.endPointKey + ' (' + item.symbol + ')'}
{...(this.state.selectedCurrency.endPointKey === item.endPointKey
? {
rightIcon: this.state.selectedNewCurrency ? (

9
screen/settings/language.js

@ -19,15 +19,18 @@ export default class Language extends Component {
availableLanguages: [
{ label: 'English', value: 'en' },
{ label: 'Česky (CZ)', value: 'cs_cz' },
{ label: 'Chinese (ZH)', value: 'zh_cn' },
{ label: 'Croatian (HR)', value: 'hr_hr' },
{ label: 'Danish (DK)', value: 'da_dk' },
{ label: 'Deutsch (DE)', value: 'de_de' },
{ label: 'Dutch (NL)', value: 'nl_nl' },
{ label: 'Español (ES)', value: 'es' },
{ label: 'Français (FR)', value: 'fr_fr' },
{ label: 'Indonesia (ID)', value: 'id_id' },
{ label: '日本語 (JP)', value: 'jp_jp' },
{ label: 'Nederlands (NL)', value: 'nl_nl' },
{ label: 'Portuguese (BR)', value: 'pt_br' },
{ label: 'Portuguese (PT)', value: 'pt_pt' },
{ label: 'Русский', value: 'ru' },
{ label: 'Spanish', value: 'es' },
{ label: 'Thai (TH)', value: 'th_th' },
{ label: 'Українська', value: 'ua' },
],
@ -77,7 +80,7 @@ export default class Language extends Component {
renderItem={this.renderItem}
/>
<BlueCard>
<BlueText>When selecting a new language, restarting Blue Wallet may be required for the change to take effect.</BlueText>
<BlueText>When selecting a new language, restarting BlueWallet may be required for the change to take effect.</BlueText>
</BlueCard>
</SafeBlueArea>
);

16
screen/settings/lightningSettings.js

@ -3,14 +3,16 @@ import { AsyncStorage, View, TextInput, Linking } from 'react-native';
import { AppStorage } from '../../class';
import { BlueLoading, BlueSpacing20, BlueButton, SafeBlueArea, BlueCard, BlueNavigationStyle, BlueText } from '../../BlueComponents';
import PropTypes from 'prop-types';
import { Button } from 'react-native-elements';
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
let loc = require('../../loc');
export default class LightningSettings extends Component {
static navigationOptions = () => ({
...BlueNavigationStyle(),
title: 'Lightning Settings',
title: loc.settings.lightning_settings,
});
constructor(props) {
@ -52,13 +54,10 @@ export default class LightningSettings extends Component {
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1 }}>
<BlueCard>
<BlueText>
To connect to your own LND node please install LndHub and put its URL here in settings. Leave blank to use default LndHub
(lndhub.io)
</BlueText>
<BlueText>{loc.settings.lightning_settings_explain}</BlueText>
</BlueCard>
<BlueButton
<Button
icon={{
name: 'mark-github',
type: 'octicon',
@ -69,6 +68,7 @@ export default class LightningSettings extends Component {
Linking.openURL('https://github.com/BlueWallet/LndHub');
}}
title="github.com/BlueWallet/LndHub"
color={BlueApp.settings.buttonTextColor}
buttonStyle={{
backgroundColor: '#FFFFFF',
}}
@ -94,7 +94,7 @@ export default class LightningSettings extends Component {
value={this.state.URI}
onChangeText={text => this.setState({ URI: text })}
numberOfLines={1}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33, height: 33 }}
style={{ flex: 1, marginHorizontal: 8, minHeight: 36, height: 36 }}
editable={!this.state.isLoading}
underlineColorAndroid="transparent"
/>
@ -105,7 +105,7 @@ export default class LightningSettings extends Component {
onPress={() => {
this.save();
}}
title={'Save'}
title={loc.settings.save}
/>
</BlueCard>
</SafeBlueArea>

51
screen/settings/releasenotes.js

@ -0,0 +1,51 @@
import React, { Component } from 'react';
import { ScrollView } from 'react-native';
import { BlueLoading, SafeBlueArea, BlueCard, BlueText, BlueNavigationStyle } from '../../BlueComponents';
import PropTypes from 'prop-types';
/** @type {AppStorage} */
const notes = require('../../release-notes');
export default class ReleaseNotes extends Component {
static navigationOptions = () => ({
...BlueNavigationStyle(),
title: 'Release notes',
});
constructor(props) {
super(props);
this.state = {
isLoading: true,
};
}
async componentDidMount() {
console.log(notes);
this.setState({
isLoading: false,
notes: notes,
});
}
render() {
if (this.state.isLoading) {
return <BlueLoading />;
}
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1 }}>
<ScrollView>
<BlueCard>
<BlueText>{this.state.notes}</BlueText>
</BlueCard>
</ScrollView>
</SafeBlueArea>
);
}
}
ReleaseNotes.propTypes = {
navigation: PropTypes.shape({
navigate: PropTypes.func,
goBack: PropTypes.func,
}),
};

2
screen/settings/settings.js

@ -39,7 +39,7 @@ export default class Settings extends Component {
<BlueListItem title={loc.settings.encrypt_storage} />
</TouchableOpacity>
<TouchableOpacity onPress={() => this.props.navigation.navigate('LightningSettings')}>
<BlueListItem title="Lightning settings" />
<BlueListItem title={loc.settings.lightning_settings} />
</TouchableOpacity>
<TouchableOpacity onPress={() => this.props.navigation.navigate('Language')}>
<BlueListItem title={loc.settings.language} />

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save