mirror of
https://github.com/RoboSats/robosats.git
synced 2025-09-13 00:56:22 +00:00
Websockets on Tor android
This commit is contained in:
@ -29,6 +29,7 @@ import {
|
|||||||
import { systemClient } from '../../services/System';
|
import { systemClient } from '../../services/System';
|
||||||
import { TorIcon } from '../Icons';
|
import { TorIcon } from '../Icons';
|
||||||
import { apiClient } from '../../services/api';
|
import { apiClient } from '../../services/api';
|
||||||
|
import { websocketClient } from '../../services/Websocket';
|
||||||
|
|
||||||
interface SettingsFormProps {
|
interface SettingsFormProps {
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
@ -198,7 +199,6 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
|
|||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
sx={{ width: '100%' }}
|
sx={{ width: '100%' }}
|
||||||
disabled={client === 'mobile'}
|
|
||||||
exclusive={true}
|
exclusive={true}
|
||||||
value={settings.connection}
|
value={settings.connection}
|
||||||
onChange={(_e, connection) => {
|
onChange={(_e, connection) => {
|
||||||
@ -249,6 +249,7 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
|
|||||||
setSettings({ ...settings, useProxy });
|
setSettings({ ...settings, useProxy });
|
||||||
systemClient.setItem('settings_use_proxy', String(useProxy));
|
systemClient.setItem('settings_use_proxy', String(useProxy));
|
||||||
apiClient.useProxy = useProxy;
|
apiClient.useProxy = useProxy;
|
||||||
|
websocketClient.useProxy = useProxy;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ToggleButton value={true} color='primary'>
|
<ToggleButton value={true} color='primary'>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ const EncryptedChat: React.FC<Props> = ({
|
|||||||
messages,
|
messages,
|
||||||
status,
|
status,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const [turtleMode, setTurtleMode] = useState<boolean>(window.ReactNativeWebView !== undefined);
|
const [turtleMode, setTurtleMode] = useState<boolean>(false);
|
||||||
|
|
||||||
return turtleMode ? (
|
return turtleMode ? (
|
||||||
<EncryptedTurtleChat
|
<EncryptedTurtleChat
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import i18n from '../i18n/Web';
|
import i18n from '../i18n/Web';
|
||||||
import { systemClient } from '../services/System';
|
import { systemClient } from '../services/System';
|
||||||
|
import { websocketClient } from '../services/Websocket';
|
||||||
import { apiClient } from '../services/api';
|
import { apiClient } from '../services/api';
|
||||||
import { getHost } from '../utils';
|
import { getHost } from '../utils';
|
||||||
|
|
||||||
@ -57,6 +58,7 @@ class BaseSettings {
|
|||||||
const useProxy = systemClient.getItem('settings_use_proxy');
|
const useProxy = systemClient.getItem('settings_use_proxy');
|
||||||
this.useProxy = client === 'mobile' && useProxy !== 'false';
|
this.useProxy = client === 'mobile' && useProxy !== 'false';
|
||||||
apiClient.useProxy = this.useProxy;
|
apiClient.useProxy = this.useProxy;
|
||||||
|
websocketClient.useProxy = this.useProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public frontend: 'basic' | 'pro' = 'basic';
|
public frontend: 'basic' | 'pro' = 'basic';
|
||||||
|
|||||||
1
frontend/src/services/Native/index.d.ts
vendored
1
frontend/src/services/Native/index.d.ts
vendored
@ -28,6 +28,7 @@ export interface NativeWebViewMessageSystem {
|
|||||||
type:
|
type:
|
||||||
| 'init'
|
| 'init'
|
||||||
| 'torStatus'
|
| 'torStatus'
|
||||||
|
| 'WsMessage'
|
||||||
| 'copyToClipboardString'
|
| 'copyToClipboardString'
|
||||||
| 'setCookie'
|
| 'setCookie'
|
||||||
| 'deleteCookie'
|
| 'deleteCookie'
|
||||||
|
|||||||
@ -45,8 +45,8 @@ class NativeRobosats {
|
|||||||
if (message.key !== undefined) {
|
if (message.key !== undefined) {
|
||||||
this.cookies[message.key] = String(message.detail);
|
this.cookies[message.key] = String(message.detail);
|
||||||
}
|
}
|
||||||
} else if (message.type === 'navigateToPage') {
|
} else {
|
||||||
window.dispatchEvent(new CustomEvent('navigateToPage', { detail: message?.detail }));
|
window.dispatchEvent(new CustomEvent(message.type, { detail: message?.detail }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,87 @@
|
|||||||
|
import { WebsocketState, type WebsocketClient, type WebsocketConnection } from '..';
|
||||||
|
import WebsocketWebClient from '../WebsocketWebClient';
|
||||||
|
|
||||||
|
class WebsocketConnectionNative implements WebsocketConnection {
|
||||||
|
constructor(path: string) {
|
||||||
|
this.path = path;
|
||||||
|
window.addEventListener('wsMessage', (event) => {
|
||||||
|
const path: string = event?.detail?.path;
|
||||||
|
const message: string = event?.detail?.message;
|
||||||
|
if (path && message && path === this.path) {
|
||||||
|
this.wsMessagePromises.forEach((fn) => fn({ data: message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly path: string;
|
||||||
|
|
||||||
|
private readonly wsMessagePromises: ((message: any) => void)[] = [];
|
||||||
|
private readonly wsClosePromises: (() => void)[] = [];
|
||||||
|
|
||||||
|
public send: (message: string) => void = (message: string) => {
|
||||||
|
window.NativeRobosats?.postMessage({
|
||||||
|
category: 'ws',
|
||||||
|
type: 'send',
|
||||||
|
path: this.path,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public close: () => void = () => {
|
||||||
|
window.NativeRobosats?.postMessage({
|
||||||
|
category: 'ws',
|
||||||
|
type: 'close',
|
||||||
|
path: this.path,
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.connection) {
|
||||||
|
this.wsClosePromises.forEach((fn) => fn());
|
||||||
|
} else {
|
||||||
|
new Error('Failed to close websocket connection.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public onMessage: (event: (message: any) => void) => void = (event) => {
|
||||||
|
this.wsMessagePromises.push(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
public onClose: (event: () => void) => void = (event) => {
|
||||||
|
this.wsClosePromises.push(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
public onError: (event: (error: any) => void) => void = (_event) => {
|
||||||
|
// Not implemented
|
||||||
|
};
|
||||||
|
|
||||||
|
public getReadyState: () => number = () => WebsocketState.OPEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebsocketNativeClient implements WebsocketClient {
|
||||||
|
public useProxy = true;
|
||||||
|
|
||||||
|
private readonly webClient: WebsocketWebClient = new WebsocketWebClient();
|
||||||
|
|
||||||
|
public open: (path: string) => Promise<WebsocketConnection> = async (path) => {
|
||||||
|
if (!this.useProxy) return await this.webClient.open(path);
|
||||||
|
|
||||||
|
return await new Promise<WebsocketConnection>((resolve, reject) => {
|
||||||
|
window.NativeRobosats?.postMessage({
|
||||||
|
category: 'ws',
|
||||||
|
type: 'open',
|
||||||
|
path,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.connection) {
|
||||||
|
resolve(new WebsocketConnectionNative(path));
|
||||||
|
} else {
|
||||||
|
reject(new Error('Failed to establish a websocket connection.'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
reject(new Error('Failed to establish a websocket connection.'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebsocketNativeClient;
|
||||||
@ -39,6 +39,8 @@ class WebsocketConnectionWeb implements WebsocketConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WebsocketWebClient implements WebsocketClient {
|
class WebsocketWebClient implements WebsocketClient {
|
||||||
|
public useProxy = false;
|
||||||
|
|
||||||
public open: (path: string) => Promise<WebsocketConnection> = async (path) => {
|
public open: (path: string) => Promise<WebsocketConnection> = async (path) => {
|
||||||
return await new Promise<WebsocketConnection>((resolve, reject) => {
|
return await new Promise<WebsocketConnection>((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import WebsocketNativeClient from './WebsocketNativeClient';
|
||||||
import WebsocketWebClient from './WebsocketWebClient';
|
import WebsocketWebClient from './WebsocketWebClient';
|
||||||
|
|
||||||
export const WebsocketState = {
|
export const WebsocketState = {
|
||||||
@ -17,7 +18,19 @@ export interface WebsocketConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface WebsocketClient {
|
export interface WebsocketClient {
|
||||||
|
useProxy: boolean;
|
||||||
open: (path: string) => Promise<WebsocketConnection>;
|
open: (path: string) => Promise<WebsocketConnection>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const websocketClient: WebsocketClient = new WebsocketWebClient();
|
function getWebsocketClient(): WebsocketClient {
|
||||||
|
if (window.navigator.userAgent.includes('robosats')) {
|
||||||
|
// If userAgent has "RoboSats", we assume the app is running inside of the
|
||||||
|
// react-native-web view of the RoboSats Android app.
|
||||||
|
return new WebsocketNativeClient();
|
||||||
|
} else {
|
||||||
|
// Otherwise, we assume the app is running in a web browser.
|
||||||
|
return new WebsocketWebClient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const websocketClient: WebsocketClient = getWebsocketClient();
|
||||||
|
|||||||
@ -7,8 +7,6 @@ class ApiNativeClient implements ApiClient {
|
|||||||
|
|
||||||
private readonly webClient: ApiClient = new ApiWebClient();
|
private readonly webClient: ApiClient = new ApiWebClient();
|
||||||
|
|
||||||
private readonly assetsPromises = new Map<string, Promise<string | undefined>>();
|
|
||||||
|
|
||||||
private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => {
|
private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => {
|
||||||
let headers = {
|
let headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -44,9 +42,9 @@ class ApiNativeClient implements ApiClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public put: (baseUrl: string, path: string, body: object) => Promise<object | undefined> = async (
|
public put: (baseUrl: string, path: string, body: object) => Promise<object | undefined> = async (
|
||||||
baseUrl,
|
_baseUrl,
|
||||||
path,
|
_path,
|
||||||
body,
|
_body,
|
||||||
) => {
|
) => {
|
||||||
return await new Promise<object>((resolve, _reject) => {
|
return await new Promise<object>((resolve, _reject) => {
|
||||||
resolve({});
|
resolve({});
|
||||||
|
|||||||
@ -41,6 +41,13 @@ const App = () => {
|
|||||||
detail: payload.torStatus,
|
detail: payload.torStatus,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
DeviceEventEmitter.addListener('WsMessage', (payload) => {
|
||||||
|
injectMessage({
|
||||||
|
category: 'ws',
|
||||||
|
type: 'wsMessage',
|
||||||
|
detail: payload,
|
||||||
|
});
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -123,7 +130,30 @@ const App = () => {
|
|||||||
|
|
||||||
const onMessage = async (event: WebViewMessageEvent) => {
|
const onMessage = async (event: WebViewMessageEvent) => {
|
||||||
const data = JSON.parse(event.nativeEvent.data);
|
const data = JSON.parse(event.nativeEvent.data);
|
||||||
if (data.category === 'http') {
|
if (data.category === 'ws') {
|
||||||
|
TorModule.getTorStatus();
|
||||||
|
if (data.type === 'open') {
|
||||||
|
torClient
|
||||||
|
.wsOpen(data.path)
|
||||||
|
.then((connection: boolean) => {
|
||||||
|
injectMessageResolve(data.id, { connection });
|
||||||
|
})
|
||||||
|
.catch((e) => onCatch(data.id, e))
|
||||||
|
.finally(TorModule.getTorStatus);
|
||||||
|
} else if (data.type === 'send') {
|
||||||
|
torClient
|
||||||
|
.wsSend(data.path, data.message)
|
||||||
|
.catch((e) => onCatch(data.id, e))
|
||||||
|
.finally(TorModule.getTorStatus);
|
||||||
|
} else if (data.type === 'close') {
|
||||||
|
torClient
|
||||||
|
.wsClose(data.path)
|
||||||
|
.then((connection: boolean) => {
|
||||||
|
injectMessageResolve(data.id, { connection });
|
||||||
|
})
|
||||||
|
.finally(TorModule.getTorStatus);
|
||||||
|
}
|
||||||
|
} else if (data.category === 'http') {
|
||||||
TorModule.getTorStatus();
|
TorModule.getTorStatus();
|
||||||
if (data.type === 'get') {
|
if (data.type === 'get') {
|
||||||
torClient
|
torClient
|
||||||
|
|||||||
@ -19,6 +19,8 @@ import org.json.JSONException;
|
|||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@ -30,10 +32,15 @@ import okhttp3.OkHttpClient;
|
|||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
import okhttp3.WebSocket;
|
||||||
|
import okhttp3.WebSocketListener;
|
||||||
|
import okio.ByteString;
|
||||||
|
|
||||||
|
|
||||||
public class TorModule extends ReactContextBaseJavaModule {
|
public class TorModule extends ReactContextBaseJavaModule {
|
||||||
private ReactApplicationContext context;
|
private ReactApplicationContext context;
|
||||||
|
private static final Map<String, WebSocket> webSockets = new HashMap<>();
|
||||||
|
|
||||||
public TorModule(ReactApplicationContext reactContext) {
|
public TorModule(ReactApplicationContext reactContext) {
|
||||||
context = reactContext;
|
context = reactContext;
|
||||||
TorKmp torKmpManager = new TorKmp((Application) context.getApplicationContext());
|
TorKmp torKmpManager = new TorKmp((Application) context.getApplicationContext());
|
||||||
@ -45,6 +52,81 @@ public class TorModule extends ReactContextBaseJavaModule {
|
|||||||
return "TorModule";
|
return "TorModule";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void sendWsSend(String path, String message, final Promise promise) {
|
||||||
|
if (webSockets.get(path) != null) {
|
||||||
|
Objects.requireNonNull(webSockets.get(path)).send(message);
|
||||||
|
promise.resolve(true);
|
||||||
|
} else {
|
||||||
|
promise.resolve(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void sendWsClose(String path, final Promise promise) {
|
||||||
|
if (webSockets.get(path) != null) {
|
||||||
|
Objects.requireNonNull(webSockets.get(path)).close(1000, "Closing connection");
|
||||||
|
promise.resolve(true);
|
||||||
|
} else {
|
||||||
|
promise.resolve(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void sendWsOpen(String path, final Promise promise) {
|
||||||
|
Log.d("Tormodule", "WebSocket opening: " + path);
|
||||||
|
OkHttpClient client = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS) // Set read timeout
|
||||||
|
.proxy(TorKmpManager.INSTANCE.getTorKmpObject().getProxy())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Create a request for the WebSocket connection
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(path) // Replace with your WebSocket URL
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Create a WebSocket listener
|
||||||
|
WebSocketListener listener = new WebSocketListener() {
|
||||||
|
@Override
|
||||||
|
public void onOpen(@NonNull WebSocket webSocket, Response response) {
|
||||||
|
Log.d("Tormodule", "WebSocket opened: " + response.message());
|
||||||
|
promise.resolve(true);
|
||||||
|
synchronized (webSockets) {
|
||||||
|
webSockets.put(path, webSocket); // Store the WebSocket instance with its URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
|
||||||
|
Log.d("Tormodule", "WebSocket Message received: " + text);
|
||||||
|
onWsMessage(path, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(@NonNull WebSocket webSocket, ByteString bytes) {
|
||||||
|
Log.d("Tormodule", "WebSocket Message received: " + bytes.hex());
|
||||||
|
onWsMessage(path, bytes.hex());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosing(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
|
||||||
|
Log.d("Tormodule", "WebSocket closing: " + reason);
|
||||||
|
synchronized (webSockets) {
|
||||||
|
webSockets.remove(path); // Remove the WebSocket instance by URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull WebSocket webSocket, Throwable t, Response response) {
|
||||||
|
Log.d("Tormodule", "WebSocket error: " + t.getMessage());
|
||||||
|
promise.resolve(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
client.newWebSocket(request, listener);
|
||||||
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void sendRequest(String action, String url, String headers, String body, final Promise promise) throws JSONException, UninitializedPropertyAccessException {
|
public void sendRequest(String action, String url, String headers, String body, final Promise promise) throws JSONException, UninitializedPropertyAccessException {
|
||||||
OkHttpClient client = new OkHttpClient.Builder()
|
OkHttpClient client = new OkHttpClient.Builder()
|
||||||
@ -160,4 +242,21 @@ public class TorModule extends ReactContextBaseJavaModule {
|
|||||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
.emit("TorNewIdentity", payload);
|
.emit("TorNewIdentity", payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onWsMessage(String path, String message) {
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
payload.putString("message", message);
|
||||||
|
payload.putString("path", path);
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("WsMessage", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onWsError(String path) {
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
payload.putString("path", path);
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("WsError", payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,9 @@ interface TorModuleInterface {
|
|||||||
start: () => void;
|
start: () => void;
|
||||||
restart: () => void;
|
restart: () => void;
|
||||||
getTorStatus: () => void;
|
getTorStatus: () => void;
|
||||||
|
sendWsOpen: (path: string) => Promise<boolean>;
|
||||||
|
sendWsClose: (path: string) => Promise<boolean>;
|
||||||
|
sendWsSend: (path: string, message: string) => Promise<boolean>;
|
||||||
sendRequest: (action: string, url: string, headers: string, body: string) => Promise<string>;
|
sendRequest: (action: string, url: string, headers: string, body: string) => Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -52,6 +52,42 @@ class TorClient {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public wsOpen: (path: string) => Promise<boolean> = async (path) => {
|
||||||
|
return await new Promise<boolean>((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
TorModule.sendWsOpen(path).then((response) => {
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public wsClose: (path: string) => Promise<boolean> = async (path) => {
|
||||||
|
return await new Promise<boolean>((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
TorModule.sendWsClose(path).then((response) => {
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public wsSend: (path: string, message: string) => Promise<boolean> = async (path, message) => {
|
||||||
|
return await new Promise<boolean>((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
TorModule.sendWsSend(path, message).then((response) => {
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TorClient;
|
export default TorClient;
|
||||||
|
|||||||
Reference in New Issue
Block a user