ADD: TOR support

This commit is contained in:
Overtorment 2021-04-23 12:29:45 +01:00
parent 2e2709816d
commit 1bdd37d0fb
23 changed files with 602 additions and 51 deletions

View File

@ -15,6 +15,7 @@ import EncryptStorage from './screen/settings/encryptStorage';
import PlausibleDeniability from './screen/plausibledeniability';
import LightningSettings from './screen/settings/lightningSettings';
import ElectrumSettings from './screen/settings/electrumSettings';
import TorSettings from './screen/settings/torSettings';
import Tools from './screen/settings/tools';
import GeneralSettings from './screen/settings/GeneralSettings';
import NetworkSettings from './screen/settings/NetworkSettings';
@ -153,6 +154,7 @@ const WalletsRoot = () => {
/>
<WalletsStack.Screen name="LightningSettings" component={LightningSettings} options={LightningSettings.navigationOptions(theme)} />
<WalletsStack.Screen name="ElectrumSettings" component={ElectrumSettings} options={ElectrumSettings.navigationOptions(theme)} />
<WalletsStack.Screen name="TorSettings" component={TorSettings} options={TorSettings.navigationOptions(theme)} />
<WalletsStack.Screen name="SettingsPrivacy" component={SettingsPrivacy} options={SettingsPrivacy.navigationOptions(theme)} />
<WalletsStack.Screen name="Tools" component={Tools} options={Tools.navigationOptions(theme)} />
<WalletsStack.Screen name="LNDViewInvoice" component={LNDViewInvoice} options={LNDViewInvoice.navigationOptions(theme)} />

18
__mocks__/react-native-tor.js vendored Normal file
View File

@ -0,0 +1,18 @@
/* global jest */
export const startIfNotStarted = jest.fn(async (key, value, callback) => {
return 666;
});
export const get = jest.fn();
export const post = jest.fn();
export const deleteMock = jest.fn();
export const stopIfRunning = jest.fn();
export const getDaemonStatus = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return { startIfNotStarted, get, post, delete: deleteMock, stopIfRunning, getDaemonStatus };
});
export default mock;

View File

@ -177,6 +177,7 @@ android {
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation files("../../node_modules/react-native-tor/android/libs/sifir_android.aar")
//noinspection GradleDynamicVersion
androidTestImplementation('com.wix:detox:+') {

View File

@ -8,3 +8,7 @@
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
-keep class com.sifir.** { *;}
-keep interface com.sifir.** { *;}
-keep enum com.sifir.** { *;}

View File

@ -14,6 +14,7 @@
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:extractNativeLibs="true"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"
android:supportsRtl="true"

View File

@ -2,7 +2,7 @@
buildscript {
ext {
minSdkVersion = 21
minSdkVersion = 26
supportLibVersion = "28.0.0"
buildToolsVersion = "29.0.2"
compileSdkVersion = 29

View File

@ -1,6 +1,6 @@
/* global alert */
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Platform, Alert } from 'react-native';
import { Alert } from 'react-native';
import { AppStorage, LegacyWallet, SegwitBech32Wallet, SegwitP2SHWallet } from '../class';
import DefaultPreference from 'react-native-default-preference';
import RNWidgetCenter from 'react-native-widget-center';
@ -9,6 +9,7 @@ const bitcoin = require('bitcoinjs-lib');
const ElectrumClient = require('electrum-client');
const reverse = require('buffer-reverse');
const BigNumber = require('bignumber.js');
const torrific = require('../blue_modules/torrific');
const storageKey = 'ELECTRUM_PEERS';
const defaultPeer = { host: 'electrum1.bluewallet.io', ssl: '443' };
@ -48,11 +49,19 @@ async function connectMain() {
usingPeer = savedPeer;
}
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
try {
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
await DefaultPreference.set(AppStorage.ELECTRUM_HOST, usingPeer.host);
await DefaultPreference.set(AppStorage.ELECTRUM_TCP_PORT, usingPeer.tcp);
await DefaultPreference.set(AppStorage.ELECTRUM_SSL_PORT, usingPeer.ssl);
if (usingPeer.host.endsWith('onion')) {
const randomPeer = await getRandomHardcodedPeer();
await DefaultPreference.set(AppStorage.ELECTRUM_HOST, randomPeer.host);
await DefaultPreference.set(AppStorage.ELECTRUM_TCP_PORT, randomPeer.tcp);
await DefaultPreference.set(AppStorage.ELECTRUM_SSL_PORT, randomPeer.ssl);
} else {
await DefaultPreference.set(AppStorage.ELECTRUM_HOST, usingPeer.host);
await DefaultPreference.set(AppStorage.ELECTRUM_TCP_PORT, usingPeer.tcp);
await DefaultPreference.set(AppStorage.ELECTRUM_SSL_PORT, usingPeer.ssl);
}
RNWidgetCenter.reloadAllTimelines();
} catch (e) {
// Must be running on Android
@ -61,19 +70,26 @@ async function connectMain() {
try {
console.log('begin connection:', JSON.stringify(usingPeer));
mainClient = new ElectrumClient(usingPeer.ssl || usingPeer.tcp, usingPeer.host, usingPeer.ssl ? 'tls' : 'tcp');
mainClient = new ElectrumClient(
usingPeer.host.endsWith('.onion') ? torrific : global.net,
global.tls,
usingPeer.ssl || usingPeer.tcp,
usingPeer.host,
usingPeer.ssl ? 'tls' : 'tcp',
);
mainClient.onError = function (e) {
if (Platform.OS === 'android' && mainConnected) {
// android sockets are buggy and dont always issue CLOSE event, which actually makes the persistence code to reconnect.
// so lets do it manually, but only if we were previously connected (mainConnected), otherwise theres other
console.log('electrum mainClient.onError():', e.message);
if (mainConnected) {
// most likely got a timeout from electrum ping. lets reconnect
// but only if we were previously connected (mainConnected), otherwise theres other
// code which does connection retries
mainClient.close();
mainConnected = false;
setTimeout(connectMain, 500);
// dropping `mainConnected` flag ensures there wont be reconnection race condition if several
// errors triggered
console.log('reconnecting after socket error');
return;
setTimeout(connectMain, usingPeer.host.endsWith('.onion') ? 4000 : 500);
}
mainConnected = false;
};
const ver = await mainClient.initElectrum({ client: 'bluewallet', version: '1.4' });
if (ver && ver[0]) {
@ -104,6 +120,7 @@ async function connectMain() {
if (connectionAttempt >= 5) {
presentNetworkErrorAlert(usingPeer);
} else {
console.log('reconnection attempt #', connectionAttempt);
setTimeout(connectMain, 500);
}
}
@ -249,8 +266,8 @@ module.exports.getConfig = async function () {
return {
host: mainClient.host,
port: mainClient.port,
status: mainClient.status ? 1 : 0,
serverName,
connected: mainClient.timeLastCall !== 0 && mainClient.status,
};
};
@ -522,18 +539,19 @@ module.exports.waitTillConnected = async function () {
waitTillConnectedInterval = setInterval(() => {
if (mainConnected) {
clearInterval(waitTillConnectedInterval);
resolve(true);
return resolve(true);
}
if (wasConnectedAtLeastOnce && mainClient.status === 1) {
clearInterval(waitTillConnectedInterval);
mainConnected = true;
resolve(true);
return resolve(true);
}
if (retriesCounter++ >= 30) {
if (wasConnectedAtLeastOnce && retriesCounter++ >= 30) {
// `wasConnectedAtLeastOnce` needed otherwise theres gona be a race condition with the code that connects
// electrum during app startup
clearInterval(waitTillConnectedInterval);
connectionAttempt = 0;
presentNetworkErrorAlert();
reject(new Error('Waiting for Electrum connection timeout'));
}
@ -600,7 +618,12 @@ module.exports.calcEstimateFeeFromFeeHistorgam = function (numberOfBlocks, feeHi
};
module.exports.estimateFees = async function () {
const histogram = await mainClient.mempool_getFeeHistogram();
let histogram;
try {
histogram = await Promise.race([mainClient.mempool_getFeeHistogram(), new Promise(resolve => setTimeout(resolve, 5000))]);
} catch (_) {}
if (!histogram) throw new Error('timeout while getting mempool_getFeeHistogram');
// fetching what electrum (which uses bitcoin core) thinks about fees:
const _fast = await module.exports.estimateFee(1);
@ -685,13 +708,20 @@ module.exports.calculateBlockTime = function (height) {
* @returns {Promise<boolean>} Whether provided host:port is a valid electrum server
*/
module.exports.testConnection = async function (host, tcpPort, sslPort) {
const client = new ElectrumClient(sslPort || tcpPort, host, sslPort ? 'tls' : 'tcp');
const client = new ElectrumClient(
host.endsWith('.onion') ? torrific : global.net,
global.tls,
sslPort || tcpPort,
host,
sslPort ? 'tls' : 'tcp',
);
client.onError = () => {}; // mute
let timeoutId = false;
try {
const rez = await Promise.race([
new Promise(resolve => {
timeoutId = setTimeout(() => resolve('timeout'), 3000);
timeoutId = setTimeout(() => resolve('timeout'), host.endsWith('.onion') ? 21000 : 5000);
}),
client.connect(),
]);
@ -714,6 +744,7 @@ module.exports.forceDisconnect = () => {
};
module.exports.hardcodedPeers = hardcodedPeers;
module.exports.getRandomHardcodedPeer = getRandomHardcodedPeer;
const splitIntoChunks = function (arr, chunkSize) {
const groups = [];

View File

@ -61,7 +61,6 @@ async function updateExchangeRate() {
try {
rate = await getFiatRate(preferredFiatCurrency.endPointKey);
} catch (Err) {
console.warn(Err);
const lastSavedExchangeRate = JSON.parse(await AsyncStorage.getItem(AppStorage.EXCHANGE_RATES));
exchangeRates['BTC_' + preferredFiatCurrency.endPointKey] = lastSavedExchangeRate['BTC_' + preferredFiatCurrency.endPointKey] * 1;
return;

287
blue_modules/torrific.js Normal file
View File

@ -0,0 +1,287 @@
import Tor from 'react-native-tor';
const tor = Tor();
/**
* TOR wrapper mimicking Frisbee interface
*/
class Torsbee {
baseURI = '';
static _testConn;
static _resolveReference;
static _rejectReference;
constructor(opts) {
opts = opts || {};
this.baseURI = opts.baseURI || this.baseURI;
}
async get(path, options) {
console.log('TOR: starting...');
const socksProxy = await tor.startIfNotStarted();
console.log('TOR: started', await tor.getDaemonStatus(), 'on local port', socksProxy);
if (path.startsWith('/') && this.baseURI.endsWith('/')) {
// oy vey, duplicate slashes
path = path.substr(1);
}
const response = {};
try {
const uri = this.baseURI + path;
console.log('TOR: requesting', uri);
const torResponse = await tor.get(uri, options?.headers || {}, true);
response.originalResponse = torResponse;
if (options?.headers['Content-Type'] === 'application/json' && torResponse.json) {
response.body = torResponse.json;
} else {
response.body = Buffer.from(torResponse.b64Data, 'base64').toString();
}
} catch (error) {
response.err = error;
console.warn(error);
}
return response;
}
async post(path, options) {
console.log('TOR: starting...');
const socksProxy = await tor.startIfNotStarted();
console.log('TOR: started', await tor.getDaemonStatus(), 'on local port', socksProxy);
if (path.startsWith('/') && this.baseURI.endsWith('/')) {
// oy vey, duplicate slashes
path = path.substr(1);
}
const uri = this.baseURI + path;
console.log('TOR: posting to', uri);
const response = {};
try {
const torResponse = await tor.post(uri, JSON.stringify(options?.body || {}), options?.headers || {}, true);
response.originalResponse = torResponse;
if (options?.headers['Content-Type'] === 'application/json' && torResponse.json) {
response.body = torResponse.json;
} else {
response.body = Buffer.from(torResponse.b64Data, 'base64').toString();
}
} catch (error) {
response.err = error;
console.warn(error);
}
return response;
}
testSocket() {
return new Promise((resolve, reject) => {
this.constructor._resolveReference = resolve;
this.constructor._rejectReference = reject;
(async () => {
console.log('testSocket...');
try {
if (!this.constructor._testConn) {
// no test conenctino exists, creating it...
await tor.startIfNotStarted();
const target = 'v7gtzf7nua6hdmb2wtqaqioqmesdb4xrlly4zwr7bvayxv2bpg665pqd.onion:50001';
this.constructor._testConn = await tor.createTcpConnection({ target }, (data, err) => {
if (err) {
return this.constructor._rejectReference(new Error(err));
}
const json = JSON.parse(data);
if (!json || typeof json.result === 'undefined')
return this.constructor._rejectReference(new Error('Unexpected response from TOR socket: ' + JSON.stringify(json)));
// conn.close();
// instead of closing connect, we will actualy re-cyce existing test connection as we
// saved it into `this.constructor.testConn`
this.constructor._resolveReference();
});
await this.constructor._testConn.write(
`{ "id": 1, "method": "blockchain.scripthash.get_balance", "params": ["716decbe1660861c3d93906cb1d98ee68b154fd4d23aed9783859c1271b52a9c"] }\n`,
);
} else {
// test connectino exists, so we are reusing it
await this.constructor._testConn.write(
`{ "id": 1, "method": "blockchain.scripthash.get_balance", "params": ["716decbe1660861c3d93906cb1d98ee68b154fd4d23aed9783859c1271b52a9c"] }\n`,
);
}
} catch (error) {
this.constructor._rejectReference(error);
}
})();
});
}
}
/**
* Wrapper for react-native-tor mimicking Socket class from NET package
*/
class TorSocket {
constructor() {
this._socket = false;
this._listeners = {};
}
setTimeout() {}
setEncoding() {}
setKeepAlive() {}
setNoDelay() {}
on(event, listener) {
this._listeners[event] = this._listeners[event] || [];
this._listeners[event].push(listener);
}
removeListener(event, listener) {
this._listeners[event] = this._listeners[event] || [];
const newListeners = [];
let found = false;
for (const savedListener of this._listeners[event]) {
// eslint-disable-next-line eqeqeq
if (savedListener == listener) {
// found our listener
found = true;
// we just skip it
} else {
// other listeners should go back to original array
newListeners.push(savedListener);
}
}
if (found) {
this._listeners[event] = newListeners;
} else {
// something went wrong, lets just cleanup all listeners
this._listeners[event] = [];
}
}
connect(port, host, callback) {
console.log('connecting TOR socket...', host, port);
(async () => {
console.log('starting tor...');
try {
await tor.startIfNotStarted();
} catch (e) {
console.warn('Could not bootstrap TOR', e);
await tor.stopIfRunning();
this._passOnEvent('error', 'Could not bootstrap TOR');
return false;
}
console.log('started tor');
const iWillConnectISwear = tor.createTcpConnection({ target: host + ':' + port, connectionTimeout: 15000 }, (data, err) => {
if (err) {
console.log('TOR socket onData error: ', err);
// this._passOnEvent('error', err);
return;
}
this._passOnEvent('data', data);
});
try {
this._socket = await Promise.race([iWillConnectISwear, new Promise(resolve => setTimeout(resolve, 21000))]);
} catch (e) {}
if (!this._socket) {
console.log('connecting TOR socket failed'); // either sleep expired or connect threw an exception
await tor.stopIfRunning();
this._passOnEvent('error', 'connecting TOR socket failed');
return false;
}
console.log('TOR socket connected:', host, port);
setTimeout(() => {
this._passOnEvent('connect', true);
callback();
}, 1000);
})();
}
_passOnEvent(event, data) {
this._listeners[event] = this._listeners[event] || [];
for (const savedListener of this._listeners[event]) {
savedListener(data);
}
}
emit(event, data) {}
end() {
console.log('trying to close TOR socket');
if (this._socket && this._socket.close) {
console.log('trying to close TOR socket SUCCESS');
return this._socket.close();
}
}
destroy() {}
write(data) {
if (this._socket && this._socket.write) {
try {
return this._socket.write(data);
} catch (error) {
console.log('this._socket.write() failed so we are issuing ERROR event', error);
this._passOnEvent('error', error);
}
} else {
console.log('TOR socket write error, socket not connected');
this._passOnEvent('error', 'TOR socket not connected');
}
}
}
module.exports.getDaemonStatus = async () => {
try {
return await tor.getDaemonStatus();
} catch (_) {
return false;
}
};
module.exports.stopIfRunning = async () => {
try {
Torsbee._testConn = false;
return await tor.stopIfRunning();
} catch (_) {
return false;
}
};
module.exports.startIfNotStarted = async () => {
try {
return await tor.startIfNotStarted();
} catch (_) {
return false;
}
};
module.exports.testSocket = async () => {
const c = new Torsbee();
return c.testSocket();
};
module.exports.testHttp = async () => {
const api = new Torsbee({
baseURI: 'http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion:80/',
});
const torResponse = await api.get('/api/tx/a84dbcf0d2550f673dda9331eea7cab86b645fd6e12049755c4b47bd238adce9', {
headers: {
'Content-Type': 'application/json',
},
});
const json = torResponse.body;
if (json.txid !== 'a84dbcf0d2550f673dda9331eea7cab86b645fd6e12049755c4b47bd238adce9')
throw new Error('TOR failure, got ' + JSON.stringify(torResponse));
};
module.exports.Torsbee = Torsbee;
module.exports.Socket = TorSocket;

View File

@ -2,6 +2,7 @@ import { LegacyWallet } from './legacy-wallet';
import Frisbee from 'frisbee';
import bolt11 from 'bolt11';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
const torrific = require('../../blue_modules/torrific');
export class LightningCustodianWallet extends LegacyWallet {
static type = 'lightningCustodianWallet';
@ -77,6 +78,12 @@ export class LightningCustodianWallet extends LegacyWallet {
this._api = new Frisbee({
baseURI: this.baseURI,
});
if (this.baseURI.indexOf('.onion') !== -1) {
this._api = new torrific.Torsbee({
baseURI: this.baseURI,
});
}
}
accessTokenExpired() {
@ -581,9 +588,14 @@ export class LightningCustodianWallet extends LegacyWallet {
}
static async isValidNodeAddress(address) {
const apiCall = new Frisbee({
baseURI: address,
});
const isTor = address.indexOf('.onion') !== -1;
const apiCall = isTor
? new torrific.Torsbee({
baseURI: address,
})
: new Frisbee({
baseURI: address,
});
const response = await apiCall.get('/getinfo', {
headers: {
'Access-Control-Allow-Origin': '*',

View File

@ -95,7 +95,7 @@
6DEB4C3B254FBF4800E9F9AA /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEB4C3A254FBF4800E9F9AA /* Colors.swift */; };
6DEB4C3C254FBF4800E9F9AA /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEB4C3A254FBF4800E9F9AA /* Colors.swift */; };
6DF25A9F249DB97E001D06F5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6DF25A9E249DB97E001D06F5 /* LaunchScreen.storyboard */; };
6DFC807024EA0B6C007B8700 /* EFQRCode in Frameworks */ = {isa = PBXBuildFile; productRef = 6DFC806F24EA0B6C007B8700 /* EFQRCode */; };
6DFC807024EA0B6C007B8700 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 6DFC806F24EA0B6C007B8700 /* SwiftPackageProductDependency */; };
6DFC807224EA2FA9007B8700 /* ViewQRCodefaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFC807124EA2FA9007B8700 /* ViewQRCodefaceController.swift */; };
764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B468CC34D5B41F3950078EF /* libsqlite3.0.tbd */; };
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B9D9B3A7B2CB4255876B67AF /* libz.tbd */; };
@ -496,7 +496,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6DFC807024EA0B6C007B8700 /* EFQRCode in Frameworks */,
6DFC807024EA0B6C007B8700 /* BuildFile in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -996,7 +996,7 @@
);
name = "BlueWalletWatch Extension";
packageProductDependencies = (
6DFC806F24EA0B6C007B8700 /* EFQRCode */,
6DFC806F24EA0B6C007B8700 /* SwiftPackageProductDependency */,
);
productName = "BlueWalletWatch Extension";
productReference = B40D4E3C225841ED00428FCC /* BlueWalletWatch Extension.appex */;
@ -1089,7 +1089,7 @@
);
mainGroup = 83CBB9F61A601CBA00E9B192;
packageReferences = (
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */,
6DFC806E24EA0B6C007B8700 /* RemoteSwiftPackageReference */,
);
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = "";
@ -1205,6 +1205,24 @@
shellPath = /bin/sh;
shellScript = "export SENTRY_PROPERTIES=sentry.properties\nexport NODE_BINARY=node\n../node_modules/@sentry/cli/bin/sentry-cli react-native xcode ../node_modules/react-native/scripts/react-native-xcode.sh\n";
};
1D2D05C5A2622FFEA3FB0BEB /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-BlueWallet/Pods-BlueWallet-frameworks.sh",
"${PODS_ROOT}/../../node_modules/react-native-tor/ios/Libsifir_ios.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Libsifir_ios.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BlueWallet/Pods-BlueWallet-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
2130DE983D1D45AC8FC45F7E /* Upload Debug Symbols to Sentry */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -1613,6 +1631,7 @@
CURRENT_PROJECT_VERSION = 310;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = A7W54YZ4WU;
ENABLE_BITCODE = NO;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
@ -1621,7 +1640,7 @@
);
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = BlueWallet/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1661,10 +1680,11 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 310;
DEVELOPMENT_TEAM = A7W54YZ4WU;
ENABLE_BITCODE = NO;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = BlueWallet/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2513,7 +2533,7 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */ = {
6DFC806E24EA0B6C007B8700 /* RemoteSwiftPackageReference */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/EFPrefix/EFQRCode.git";
requirement = {
@ -2524,9 +2544,9 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
6DFC806F24EA0B6C007B8700 /* EFQRCode */ = {
6DFC806F24EA0B6C007B8700 /* SwiftPackageProductDependency */ = {
isa = XCSwiftPackageProductDependency;
package = 6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */;
package = 6DFC806E24EA0B6C007B8700 /* RemoteSwiftPackageReference */;
productName = EFQRCode;
};
/* End XCSwiftPackageProductDependency section */

View File

@ -1,4 +1,4 @@
platform :ios, '10.0'
platform :ios, '11.1'
workspace 'BlueWallet'
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

View File

@ -274,6 +274,8 @@ PODS:
- react-native-tcp-socket (3.7.1):
- CocoaAsyncSocket
- React
- react-native-tor (0.1.7):
- React
- react-native-webview (11.3.1):
- React-Core
- react-native-widget-center (0.0.4):
@ -452,6 +454,7 @@ DEPENDENCIES:
- react-native-randombytes (from `../node_modules/react-native-randombytes`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-tcp-socket (from `../node_modules/react-native-tcp-socket`)
- react-native-tor (from `../node_modules/react-native-tor`)
- react-native-webview (from `../node_modules/react-native-webview`)
- react-native-widget-center (from `../node_modules/react-native-widget-center`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
@ -571,6 +574,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-safe-area-context"
react-native-tcp-socket:
:path: "../node_modules/react-native-tcp-socket"
react-native-tor:
:path: "../node_modules/react-native-tor"
react-native-webview:
:path: "../node_modules/react-native-webview"
react-native-widget-center:
@ -701,6 +706,7 @@ SPEC CHECKSUMS:
react-native-randombytes: 45ae693012df24c9a07a5e578b3b567c01468581
react-native-safe-area-context: e471852c5ed67eea4b10c5d9d43c1cebae3b231d
react-native-tcp-socket: 96a4f104cdcc9c6621aafe92937f163d88447c5b
react-native-tor: 4f389f5719dad633542b57ea32744e954730e7ef
react-native-webview: 30f048378c6cee522ed9bbbedbc34acb21e58188
react-native-widget-center: 0f81d17beb163e7fb5848b06754d7d277fe7d99a
React-RCTActionSheet: 89a0ca9f4a06c1f93c26067af074ccdce0f40336
@ -744,6 +750,6 @@ SPEC CHECKSUMS:
Yoga: 4bd86afe9883422a7c4028c00e34790f560923d6
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
PODFILE CHECKSUM: 070fb164a2dfb94605ea7862fd9b1c25e95ff046
PODFILE CHECKSUM: 8fb37a9b658fd4d511d97e40f40e2d511092b261
COCOAPODS: 1.10.1

View File

@ -276,6 +276,7 @@
"electrum_history": "Server history",
"electrum_reset_to_default": "Are you sure to want to reset your Electrum settings to default?",
"electrum_clear": "Clear",
"tor_supported": "TOR supported",
"encrypt_decrypt": "Decrypt Storage",
"encrypt_decrypt_q": "Are you sure you want to decrypt your storage? This will allow your wallets to be accessed without a password.",
"encrypt_del_uninstall": "Delete if BlueWallet is uninstalled",
@ -296,6 +297,7 @@
"lightning_error_lndhub_uri": "Not a valid LNDHub URI",
"lightning_saved": "Your changes have been saved successfully.",
"lightning_settings": "Lightning Settings",
"tor_settings": "TOR 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 BlueWallets LNDHub (lndhub.io). Wallets created after saving changes will connect to the specified LNDHub.",
"network": "Network",
"network_broadcast": "Broadcast Transaction",

27
package-lock.json generated
View File

@ -6075,6 +6075,11 @@
"@sinonjs/commons": "^1.7.0"
}
},
"@types/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.6.tgz",
"integrity": "sha512-ZkrXnZLC1mc4b9QLKaSrsxV4oxTRs10OI2kgSApT8G0v1jrmqppSHUVQ15kLorzsFBTjvf7OKF4kAibuuNQ+xA=="
},
"@types/babel__core": {
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.9.tgz",
@ -9120,8 +9125,8 @@
"integrity": "sha512-rQItBTFnol20HaaLm26UgSUduX7iGerwW7pEYX17MB1tI6LzFajiLV7iZ7LVcUcsN/7HrZUoCLrBauChy/IqEg=="
},
"electrum-client": {
"version": "git+ssh://git@github.com/BlueWallet/rn-electrum-client.git#f9a827e724a5a2e578fdfdb483f83793af55b030",
"from": "electrum-client@git+https://github.com/BlueWallet/rn-electrum-client.git#f9a827e724a5a2e578fdfdb483f83793af55b030"
"version": "git+https://github.com/BlueWallet/rn-electrum-client.git#99ebcc649d91a8dc39bea7964b02dd9ead464aa4",
"from": "git+https://github.com/BlueWallet/rn-electrum-client.git#99ebcc649d91a8dc39bea7964b02dd9ead464aa4"
},
"electrum-mnemonic": {
"version": "2.0.0",
@ -18815,6 +18820,22 @@
"version": "git+ssh://git@github.com/BlueWallet/react-native-tooltip.git#d369e7ece09e4dec73873f1cfeac83e9d35294a6",
"from": "react-native-tooltip@git+https://github.com/BlueWallet/react-native-tooltip.git#d369e7ece09e4dec73873f1cfeac83e9d35294a6"
},
"react-native-tor": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/react-native-tor/-/react-native-tor-0.1.7.tgz",
"integrity": "sha512-jIIo4at/cD/Hy6y/Ae+509XeJ/47AvAoRBJBOV0CsmpAh6WacBmIrkiEBv1SUqh2pkbXG6YGn3CRJFowbc5UYw==",
"requires": {
"@types/async": "^3.2.6",
"async": "^3.2.0"
},
"dependencies": {
"async": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
}
}
},
"react-native-vector-icons": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-7.1.0.tgz",
@ -19656,7 +19677,7 @@
}
},
"scryptsy": {
"version": "file:blue_modules/scryptsy",
"version": "file:https:/registry.npmjs.org/scryptsy/-/scryptsy-2.1.0.tgz",
"integrity": "sha512-1CdSqHQowJBnMAFyPEBRfqag/YP9OF394FV+4YREIJX4ljD7OxvQRDayyoyyCk+senRjSkP6VnUNQmVQqB6g7w=="
},
"secp256k1": {

View File

@ -102,7 +102,7 @@
"dayjs": "1.10.4",
"detox": "18.8.1",
"ecurve": "1.0.6",
"electrum-client": "git+https://github.com/BlueWallet/rn-electrum-client.git#f9a827e724a5a2e578fdfdb483f83793af55b030",
"electrum-client": "git+https://github.com/BlueWallet/rn-electrum-client.git#99ebcc649d91a8dc39bea7964b02dd9ead464aa4",
"electrum-mnemonic": "2.0.0",
"eslint-config-prettier": "6.14.0",
"eslint-config-standard": "14.1.1",
@ -164,6 +164,7 @@
"react-native-svg": "12.1.0",
"react-native-tcp-socket": "3.7.1",
"react-native-tooltip": "git+https://github.com/BlueWallet/react-native-tooltip.git#d369e7ece09e4dec73873f1cfeac83e9d35294a6",
"react-native-tor": "0.1.7",
"react-native-vector-icons": "7.1.0",
"react-native-watch-connectivity": "1.0.3",
"react-native-webview": "11.3.1",

View File

@ -10,6 +10,7 @@ const bitcoin = require('bitcoinjs-lib');
const BlueCrypto = require('react-native-blue-crypto');
const encryption = require('../blue_modules/encryption');
const BlueElectrum = require('../blue_modules/BlueElectrum');
const torrific = require('../blue_modules/torrific');
const styles = StyleSheet.create({
center: {
@ -46,6 +47,13 @@ export default class Selftest extends Component {
//
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
await torrific.testHttp();
await torrific.testSocket();
} else {
// skipping RN-specific test'
}
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();

View File

@ -15,10 +15,12 @@ import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc';
import { BlueCurrentTheme } from '../../components/themes';
import Notifications from '../../blue_modules/notifications';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import { Psbt } from 'bitcoinjs-lib';
const currency = require('../../blue_modules/currency');
const BlueElectrum = require('../../blue_modules/BlueElectrum');
const Bignumber = require('bignumber.js');
const bitcoin = require('bitcoinjs-lib');
const torrific = require('../../blue_modules/torrific');
export default class Confirm extends Component {
static contextType = BlueStorageContext;
@ -69,11 +71,38 @@ export default class Confirm extends Component {
} else {
const wallet = new PayjoinTransaction(this.state.psbt, txHex => this.broadcast(txHex), this.state.fromWallet);
const paymentScript = this.getPaymentScript();
const payjoinClient = new PayjoinClient({
paymentScript,
wallet,
payjoinUrl: this.state.payjoinUrl,
});
let payjoinClient;
if (this.state.payjoinUrl.includes('.onion')) {
console.warn('trying TOR....');
const payjoinUrl = this.state.payjoinUrl;
// working through TOR - crafting custom requester that will handle TOR http request
const customPayjoinRequester = {
requestPayjoin: async function (psbt: Psbt) {
console.warn('requesting payjoin with psbt:', psbt.toBase64());
const api = new torrific.Torsbee();
const torResponse = await api.post(payjoinUrl, {
headers: {
'Content-Type': 'text/plain',
},
body: psbt.toBase64(),
});
console.warn('got torResponse.body');
if (!torResponse.body) throw new Error('TOR failure, got ' + JSON.stringify(torResponse));
return Psbt.fromBase64(torResponse.body);
},
};
payjoinClient = new PayjoinClient({
paymentScript,
wallet,
payjoinRequester: customPayjoinRequester,
});
} else {
payjoinClient = new PayjoinClient({
paymentScript,
wallet,
payjoinUrl: this.state.payjoinUrl,
});
}
await payjoinClient.run();
const payjoinPsbt = wallet.getPayjoinPsbt();
if (payjoinPsbt) {

View File

@ -13,6 +13,10 @@ const NetworkSettings = () => {
navigate('ElectrumSettings');
};
const navigateToTorSettings = () => {
navigate('TorSettings');
};
const navigateToLightningSettings = () => {
navigate('LightningSettings');
};
@ -30,6 +34,7 @@ const NetworkSettings = () => {
chevron
/>
)}
<BlueListItem title={loc.settings.tor_settings} onPress={navigateToTorSettings} testID="TorSettings" chevron />
</ScrollView>
</SafeBlueArea>
);

View File

@ -220,9 +220,11 @@ export default class ElectrumSettings extends Component {
<BlueCard>
<BlueText style={styles.status}>{loc.settings.electrum_status}</BlueText>
<View style={styles.connectWrap}>
<View style={[styles.container, this.state.config.status === 1 ? styles.containerConnected : styles.containerDisconnected]}>
<BlueText style={this.state.config.status === 1 ? styles.textConnected : styles.textDisconnected}>
{this.state.config.status === 1 ? loc.settings.electrum_connected : loc.settings.electrum_connected_not}
<View
style={[styles.container, this.state.config.connected === 1 ? styles.containerConnected : styles.containerDisconnected]}
>
<BlueText style={this.state.config.connected === 1 ? styles.textConnected : styles.textDisconnected}>
{this.state.config.connected === 1 ? loc.settings.electrum_connected : loc.settings.electrum_connected_not}
</BlueText>
</View>
</View>
@ -287,6 +289,7 @@ export default class ElectrumSettings extends Component {
testID="SSLPortInput"
/>
</View>
<BlueText style={styles.torSupported}>{loc.settings.tor_supported}</BlueText>
<BlueSpacing20 />
<BlueButtonLink title={loc.wallets.import_scan_qr} onPress={this.importScan} />
<BlueSpacing20 />
@ -402,4 +405,7 @@ const styles = StyleSheet.create({
borderBottomColor: BlueCurrentTheme.colors.formBorder,
borderBottomWidth: 1,
},
torSupported: {
color: '#81868e',
},
});

View File

@ -37,6 +37,9 @@ const styles = StyleSheet.create({
backgroundColor: 'transparent',
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
},
torSupported: {
color: '#81868e',
},
});
const LightningSettings = () => {
@ -144,6 +147,8 @@ const LightningSettings = () => {
/>
</View>
<BlueText style={[styles.torSupported]}>{loc.settings.tor_supported}</BlueText>
<BlueSpacing20 />
<BlueButtonLink title={loc.wallets.import_scan_qr} testID="ImportScan" onPress={importScan} />
<BlueSpacing20 />
{isLoading ? <BlueLoading /> : <BlueButton testID="Save" onPress={save} title={loc.settings.save} />}

View File

@ -0,0 +1,93 @@
/* global alert */
import React, { useState, useEffect } from 'react';
import { View, StyleSheet } from 'react-native';
import navigationStyle from '../../components/navigationStyle';
import { BlueButton, BlueCard, BlueLoading, BlueSpacing20, BlueText, SafeBlueArea } from '../../BlueComponents';
import loc from '../../loc';
const torrific = require('../../blue_modules/torrific');
const styles = StyleSheet.create({
torSupported: {
color: '#81868e',
},
});
const TorSettings = () => {
const [isLoading, setIsLoading] = useState(false);
const [daemonStatus, setDaemonStatus] = useState('');
const updateStatus = async () => {
const status = await torrific.getDaemonStatus();
setDaemonStatus(status);
};
const startIfNotStarted = async () => {
await torrific.startIfNotStarted();
};
const stopIfRunning = async () => {
await torrific.stopIfRunning();
};
const testSocket = async () => {
try {
setIsLoading(true);
await torrific.testSocket();
alert('OK');
} catch (error) {
alert(error.message);
} finally {
setIsLoading(false);
}
};
const testHttp = async () => {
try {
setIsLoading(true);
await torrific.testHttp();
alert('OK');
} catch (error) {
alert(error.message);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
const interval = setInterval(updateStatus, 1000);
return () => {
clearInterval(interval);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (isLoading) {
return (
<View style={[styles.root]}>
<BlueLoading />
</View>
);
}
return (
<SafeBlueArea>
<BlueCard>
<BlueText>Daemon Status: {daemonStatus}</BlueText>
</BlueCard>
<BlueButton title="start" onPress={startIfNotStarted} />
<BlueSpacing20 />
<BlueButton title="stop" onPress={stopIfRunning} />
<BlueSpacing20 />
<BlueButton title="test socket" onPress={testSocket} />
<BlueSpacing20 />
<BlueButton title="test http" onPress={testHttp} />
</SafeBlueArea>
);
};
TorSettings.navigationOptions = navigationStyle({}, opts => ({ ...opts, title: loc.settings.tor_settings }));
export default TorSettings;

View File

@ -1,6 +1,6 @@
const bitcoin = require('bitcoinjs-lib');
global.net = require('net');
global.tls = require('tls');
const net = require('net');
const tls = require('tls');
const assert = require('assert');
jasmine.DEFAULT_TIMEOUT_INTERVAL = 150 * 1000;
@ -19,7 +19,7 @@ describe('ElectrumClient', () => {
const ElectrumClient = require('electrum-client');
for (const peer of hardcodedPeers) {
const mainClient = new ElectrumClient(peer.ssl || peer.tcp, peer.host, peer.ssl ? 'tls' : 'tcp');
const mainClient = new ElectrumClient(net, tls, peer.ssl || peer.tcp, peer.host, peer.ssl ? 'tls' : 'tcp');
try {
await mainClient.connect();