Merge pull request #1262 from RoboSats/new-tor-engine

Fix Federation Android App
This commit is contained in:
Reckless_Satoshi 2024-05-01 19:09:25 +00:00 committed by GitHub
commit 0f41a613e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 935 additions and 256 deletions

3
.gitignore vendored
View File

@ -1,9 +1,6 @@
*.py[cod]
__pycache__
# C extensions
*.so
# Packages
*.egg
*.egg-info

View File

@ -178,7 +178,7 @@ const Onboarding = ({
/>
</Grid>
{slot?.hashId ? (
{slot?.nickname ? (
<Grid item>
<Typography align='center'>{t('Hi! My name is')}</Typography>
<Typography component='h5' variant='h5'>

View File

@ -90,7 +90,7 @@ const RobotProfile = ({
sx={{ width: '100%' }}
>
<Grid item sx={{ height: '2.3em', position: 'relative' }}>
{slot?.hashId ? (
{slot?.nickname ? (
<Typography align='center' component='h5' variant='h5'>
<div
style={{

View File

@ -44,7 +44,7 @@ const RobotPage = (): JSX.Element => {
const token = urlToken ?? garage.currentSlot;
if (token !== undefined && token !== null && page === 'robot') {
setInputToken(token);
if (window.NativeRobosats === undefined || torStatus === '"Done"') {
if (window.NativeRobosats === undefined || torStatus === 'ON') {
getGenerateRobot(token);
setView('profile');
}
@ -83,7 +83,7 @@ const RobotPage = (): JSX.Element => {
garage.deleteSlot();
};
if (!(window.NativeRobosats === undefined) && !(torStatus === 'DONE' || torStatus === '"Done"')) {
if (!(window.NativeRobosats === undefined) && !(torStatus === 'ON')) {
return (
<Paper
elevation={12}

View File

@ -45,7 +45,6 @@ const ClickThroughDataGrid = styled(DataGrid)({
'& .MuiDataGrid-overlayWrapperInner': {
pointerEvents: 'none',
},
...{ headerStyleFix },
});
const premiumColor = function (baseColor: string, accentColor: string, point: number): string {
@ -897,6 +896,11 @@ const BookTable = ({
: orders;
}, [showControls, orders, fav, paymentMethods]);
const loadingPercentage =
((federation.exchange.enabledCoordinators - federation.exchange.loadingCoordinators) /
federation.exchange.enabledCoordinators) *
100;
if (!fullscreen) {
return (
<Paper
@ -908,6 +912,7 @@ const BookTable = ({
}
>
<ClickThroughDataGrid
sx={headerStyleFix}
localeText={localeText}
rowHeight={3.714 * theme.typography.fontSize}
headerHeight={3.25 * theme.typography.fontSize}
@ -928,12 +933,8 @@ const BookTable = ({
setPaymentMethods,
},
loadingOverlay: {
variant: 'determinate',
value:
((federation.exchange.enabledCoordinators -
federation.exchange.loadingCoordinators) /
federation.exchange.enabledCoordinators) *
100,
variant: loadingPercentage === 0 ? 'indeterminate' : 'determinate',
value: loadingPercentage,
},
}}
paginationModel={paginationModel}
@ -949,6 +950,7 @@ const BookTable = ({
<Dialog open={fullscreen} fullScreen={true}>
<Paper style={{ width: '100%', height: '100%', overflow: 'auto' }}>
<ClickThroughDataGrid
sx={headerStyleFix}
localeText={localeText}
rowHeight={3.714 * theme.typography.fontSize}
headerHeight={3.25 * theme.typography.fontSize}

View File

@ -64,7 +64,7 @@ const ProfileDialog = ({ open = false, onClose }: Props): JSX.Element => {
<ListItem className='profileNickname'>
<ListItemText>
<Typography component='h6' variant='h6'>
{garage.getSlot()?.nickname !== undefined && (
{!garage.getSlot()?.nickname && (
<div style={{ position: 'relative', left: '-7px' }}>
<div
style={{

View File

@ -3,8 +3,8 @@ import SmoothImage from 'react-smooth-image';
import { Avatar, Badge, Tooltip } from '@mui/material';
import { SendReceiveIcon } from '../Icons';
import placeholder from './placeholder.json';
import { robohash } from './RobohashGenerator';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { roboidentitiesClient } from '../../services/Roboidentities/Web';
interface Props {
shortAlias?: string | undefined;
@ -54,10 +54,9 @@ const RobotAvatar: React.FC<Props> = ({
const className = placeholderType === 'loading' ? 'loadingAvatar' : 'generatingAvatar';
useEffect(() => {
// TODO: HANDLE ANDROID AVATARS TOO (when window.NativeRobosats !== undefined)
if (hashId !== undefined) {
robohash
.generate(hashId, small ? 'small' : 'large')
roboidentitiesClient
.generateRobohash(hashId, small ? 'small' : 'large')
.then((avatar) => {
setAvatarSrc(avatar);
})
@ -78,9 +77,7 @@ const RobotAvatar: React.FC<Props> = ({
);
} else {
setAvatarSrc(
`file:///android_asset/Web.bundle/assets/federation/avatars/${shortAlias}${
small ? ' .small' : ''
}.webp`,
`file:///android_asset/Web.bundle/assets/federation/avatars/${shortAlias}.webp`,
);
}
setTimeout(() => {

View File

@ -95,7 +95,6 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
(signedInvoice) => {
console.log('Signed message:', signedInvoice);
void coordinator.fetchReward(signedInvoice, garage, slot?.token).then((data) => {
console.log(data);
setBadInvoice(data.bad_invoice ?? '');
setShowRewardsSpinner(false);
setWithdrawn(data.successful_withdrawal);

View File

@ -226,7 +226,6 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
value={settings.network}
onChange={(e, network) => {
setSettings({ ...settings, network });
void federation.updateUrls(origin, { ...settings, network }, hostUrl);
systemClient.setItem('settings_network', network);
}}
>

View File

@ -62,7 +62,7 @@ const TorConnectionBadge = (): JSX.Element => {
return <></>;
}
if (torStatus === 'NOTINIT') {
if (torStatus === 'OFF' || torStatus === 'STOPPING') {
return (
<TorIndicator
color='primary'
@ -80,7 +80,7 @@ const TorConnectionBadge = (): JSX.Element => {
title={t('Connecting to TOR network')}
/>
);
} else if (torStatus === '"Done"' || torStatus === 'DONE') {
} else if (torStatus === 'ON') {
return <TorIndicator color='success' progress={false} title={t('Connected to TOR network')} />;
} else {
return (

View File

@ -62,7 +62,7 @@ const TorConnectionBadge = (): JSX.Element => {
return <></>;
}
if (torStatus === 'NOTINIT') {
if (torStatus === 'OFF' || torStatus === 'STOPING') {
return (
<TorIndicator
color='primary'
@ -80,7 +80,7 @@ const TorConnectionBadge = (): JSX.Element => {
title={t('Connecting to TOR network')}
/>
);
} else if (torStatus === '"Done"' || torStatus === 'DONE') {
} else if (torStatus === 'ON') {
return <TorIndicator color='success' progress={false} title={t('Connected to TOR network')} />;
} else {
return (

View File

@ -37,7 +37,7 @@ export interface SlideDirection {
out: 'left' | 'right' | undefined;
}
export type TorStatus = 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
export type TorStatus = 'ON' | 'STARTING' | 'STOPPING' | 'OFF';
export const isNativeRoboSats = !(window.NativeRobosats === undefined);
@ -155,8 +155,8 @@ export interface UseAppStoreType {
export const initialAppContext: UseAppStoreType = {
theme: undefined,
torStatus: 'NOTINIT',
settings: getSettings(),
torStatus: 'STARTING',
settings: new Settings(),
setSettings: () => {},
page: entryPage,
setPage: () => {},
@ -225,7 +225,7 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
() => {
setTorStatus(event?.detail);
},
event?.detail === '"Done"' ? 5000 : 0,
event?.detail === 'ON' ? 5000 : 0,
);
});
}, []);

View File

@ -9,12 +9,13 @@ import React, {
type ReactNode,
} from 'react';
import { type Order, Federation } from '../models';
import { type Order, Federation, Settings } from '../models';
import { federationLottery } from '../utils';
import { AppContext, type UseAppStoreType } from './AppContext';
import { GarageContext, type UseGarageStoreType } from './GarageContext';
import NativeRobosats from '../services/Native';
// Refresh delays (ms) according to Order status
const defaultDelay = 5000;
@ -61,7 +62,7 @@ export interface UseFederationStoreType {
}
export const initialFederationContext: UseFederationStoreType = {
federation: new Federation(),
federation: new Federation('onion', new Settings(), ''),
sortedCoordinators: [],
setDelay: () => {},
currentOrderId: { id: null, shortAlias: null },
@ -79,7 +80,7 @@ export const FederationContextProvider = ({
const { settings, page, origin, hostUrl, open, torStatus } =
useContext<UseAppStoreType>(AppContext);
const { setMaker, garage, setBadOrder } = useContext<UseGarageStoreType>(GarageContext);
const [federation, setFederation] = useState(initialFederationContext.federation);
const [federation] = useState(new Federation(origin, settings, hostUrl));
const sortedCoordinators = useMemo(() => federationLottery(federation), []);
const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState<string>(
new Date().toISOString(),
@ -101,19 +102,20 @@ export const FederationContextProvider = ({
setMaker((maker) => {
return { ...maker, coordinator: sortedCoordinators[0] };
}); // default MakerForm coordinator is decided via sorted lottery
federation.registerHook('onFederationUpdate', () => {
setFederationUpdatedAt(new Date().toISOString());
});
federation.registerHook('onCoordinatorUpdate', () => {
setCoordinatorUpdatedAt(new Date().toISOString());
});
}, []);
useEffect(() => {
// On bitcoin network change we reset book, limits and federation info and fetch everything again
const newFed = initialFederationContext.federation;
newFed.registerHook('onFederationUpdate', () => {
setFederationUpdatedAt(new Date().toISOString());
});
newFed.registerHook('onCoordinatorUpdate', () => {
setCoordinatorUpdatedAt(new Date().toISOString());
});
void newFed.start(origin, settings, hostUrl);
setFederation(newFed);
if (window.NativeRobosats === undefined || torStatus === 'ON') {
void federation.updateUrl(origin, settings, hostUrl);
void federation.update();
}
}, [settings.network, torStatus]);
const onOrderReceived = (order: Order): void => {

View File

@ -6,11 +6,11 @@ import {
type Order,
type Garage,
} from '.';
import { roboidentitiesClient } from '../services/Roboidentities/Web';
import { apiClient } from '../services/api';
import { validateTokenEntropy } from '../utils';
import { compareUpdateLimit } from './Limit.model';
import { defaultOrder } from './Order.model';
import { robohash } from '../components/RobotAvatar/RobohashGenerator';
export interface Contact {
nostr?: string | undefined;
@ -97,7 +97,7 @@ function calculateSizeLimit(inputDate: Date): number {
}
export class Coordinator {
constructor(value: any) {
constructor(value: any, origin: Origin, settings: Settings, hostUrl: string) {
const established = new Date(value.established);
this.longAlias = value.longAlias;
this.shortAlias = value.shortAlias;
@ -115,6 +115,8 @@ export class Coordinator {
this.testnetNodesPubkeys = value.testnetNodesPubkeys;
this.url = '';
this.basePath = '';
this.updateUrl(origin, settings, hostUrl);
}
// These properties are loaded from federation.json
@ -145,22 +147,7 @@ export class Coordinator {
public loadingLimits: boolean = false;
public loadingRobot: boolean = true;
start = async (
origin: Origin,
settings: Settings,
hostUrl: string,
onUpdate: (shortAlias: string) => void = () => {},
): Promise<void> => {
if (this.enabled !== true) return;
void this.updateUrl(settings, origin, hostUrl, onUpdate);
};
updateUrl = async (
settings: Settings,
origin: Origin,
hostUrl: string,
onUpdate: (shortAlias: string) => void = () => {},
): Promise<void> => {
updateUrl = (origin: Origin, settings: Settings, hostUrl: string): void => {
if (settings.selfhostedClient && this.shortAlias !== 'local') {
this.url = hostUrl;
this.basePath = `/${settings.network}/${this.shortAlias}`;
@ -168,9 +155,6 @@ export class Coordinator {
this.url = String(this[settings.network][origin]);
this.basePath = '';
}
void this.update(() => {
onUpdate(this.shortAlias);
});
};
update = async (onUpdate: (shortAlias: string) => void = () => {}): Promise<void> => {
@ -191,7 +175,7 @@ export class Coordinator {
generateAllMakerAvatars = async (data: [PublicOrder]): Promise<void> => {
for (const order of data) {
void robohash.generate(order.maker_hash_id, 'small');
roboidentitiesClient.generateRobohash(order.maker_hash_id, 'small');
}
};
@ -370,7 +354,6 @@ export class Coordinator {
return await apiClient
.get(this.url, `${this.basePath}/api/order/?order_id=${orderId}`, authHeaders)
.then((data) => {
console.log('data', data);
const order: Order = {
...defaultOrder,
...data,

View File

@ -14,14 +14,14 @@ import { updateExchangeInfo } from './Exchange.model';
type FederationHooks = 'onCoordinatorUpdate' | 'onFederationUpdate';
export class Federation {
constructor() {
constructor(origin: Origin, settings: Settings, hostUrl: string) {
this.coordinators = Object.entries(defaultFederation).reduce(
(acc: Record<string, Coordinator>, [key, value]: [string, any]) => {
if (getHost() !== '127.0.0.1:8000' && key === 'local') {
// Do not add `Local Dev` unless it is running on localhost
return acc;
} else {
acc[key] = new Coordinator(value);
acc[key] = new Coordinator(value, origin, settings, hostUrl);
return acc;
}
},
@ -36,7 +36,16 @@ export class Federation {
onCoordinatorUpdate: [],
onFederationUpdate: [],
};
this.loading = true;
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
const host = getHost();
const url = `${window.location.protocol}//${host}`;
const tesnetHost = Object.values(this.coordinators).find((coor) => {
return Object.values(coor.testnet).includes(url);
});
if (tesnetHost) settings.network = 'testnet';
}
public coordinators: Record<string, Coordinator>;
@ -69,38 +78,10 @@ export class Federation {
this.triggerHook('onFederationUpdate');
};
// Setup
start = async (origin: Origin, settings: Settings, hostUrl: string): Promise<void> => {
const onCoordinatorStarted = (): void => {
this.exchange.onlineCoordinators = this.exchange.onlineCoordinators + 1;
this.onCoordinatorSaved();
};
this.loading = true;
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
const host = getHost();
const url = `${window.location.protocol}//${host}`;
const tesnetHost = Object.values(this.coordinators).find((coor) => {
return Object.values(coor.testnet).includes(url);
});
if (tesnetHost) settings.network = 'testnet';
updateUrl = async (origin: Origin, settings: Settings, hostUrl: string): Promise<void> => {
for (const coor of Object.values(this.coordinators)) {
if (coor.enabled) {
await coor.start(origin, settings, hostUrl, onCoordinatorStarted);
}
coor.updateUrl(origin, settings, hostUrl);
}
this.updateEnabledCoordinators();
};
// On Testnet/Mainnet change
updateUrls = async (origin: Origin, settings: Settings, hostUrl: string): Promise<void> => {
this.loading = true;
for (const coor of Object.values(this.coordinators)) {
await coor.updateUrl(settings, origin, hostUrl);
}
this.loading = false;
};
update = async (): Promise<void> => {
@ -115,9 +96,12 @@ export class Federation {
lifetime_volume: 0,
version: { major: 0, minor: 0, patch: 0 },
};
this.exchange.onlineCoordinators = 0;
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
this.updateEnabledCoordinators();
for (const coor of Object.values(this.coordinators)) {
await coor.update(() => {
this.exchange.onlineCoordinators = this.exchange.onlineCoordinators + 1;
this.onCoordinatorSaved();
});
}

View File

@ -59,7 +59,9 @@ class Garage {
const rawSlots = JSON.parse(slotsDump);
Object.values(rawSlots).forEach((rawSlot: Record<any, any>) => {
if (rawSlot?.token) {
this.slots[rawSlot.token] = new Slot(rawSlot.token, Object.keys(rawSlot.robots), {});
this.slots[rawSlot.token] = new Slot(rawSlot.token, Object.keys(rawSlot.robots), {}, () =>
this.triggerHook('onRobotUpdate'),
);
Object.keys(rawSlot.robots).forEach((shortAlias) => {
const rawRobot = rawSlot.robots[shortAlias];
@ -113,9 +115,10 @@ class Garage {
if (!token || !shortAliases) return;
if (this.getSlot(token) === null) {
this.slots[token] = new Slot(token, shortAliases, attributes);
this.slots[token] = new Slot(token, shortAliases, attributes, () =>
this.triggerHook('onRobotUpdate'),
);
this.save();
this.triggerHook('onRobotUpdate');
}
};

View File

@ -1,17 +1,24 @@
import { sha256 } from 'js-sha256';
import { Robot, type Order } from '.';
import { robohash } from '../components/RobotAvatar/RobohashGenerator';
import { generate_roboname } from 'robo-identities-wasm';
import { roboidentitiesClient } from '../services/Roboidentities/Web';
class Slot {
constructor(token: string, shortAliases: string[], robotAttributes: Record<any, any>) {
constructor(
token: string,
shortAliases: string[],
robotAttributes: Record<any, any>,
onRobotUpdate: () => void,
) {
this.token = token;
this.hashId = sha256(sha256(this.token));
this.nickname = generate_roboname(this.hashId);
// trigger RoboHash avatar generation in webworker and store in RoboHash class cache.
void robohash.generate(this.hashId, 'small');
void robohash.generate(this.hashId, 'large');
this.nickname = null;
roboidentitiesClient.generateRoboname(this.hashId).then((nickname) => {
this.nickname = nickname;
onRobotUpdate();
});
roboidentitiesClient.generateRobohash(this.hashId, 'small');
roboidentitiesClient.generateRobohash(this.hashId, 'large');
this.robots = shortAliases.reduce((acc: Record<string, Robot>, shortAlias: string) => {
acc[shortAlias] = new Robot(robotAttributes);
@ -22,6 +29,7 @@ class Slot {
this.activeShortAlias = null;
this.lastShortAlias = null;
this.copiedToken = false;
onRobotUpdate();
}
token: string | null;

View File

@ -15,7 +15,7 @@ export interface ReactNativeWebView {
export interface NativeWebViewMessageHttp {
id?: number;
category: 'http';
type: 'post' | 'get' | 'put' | 'delete' | 'xhr';
type: 'post' | 'get' | 'put' | 'delete';
path: string;
baseUrl: string;
headers?: object;
@ -30,7 +30,19 @@ export interface NativeWebViewMessageSystem {
detail?: string;
}
export declare type NativeWebViewMessage = NativeWebViewMessageHttp | NativeWebViewMessageSystem;
export interface NativeWebViewMessageRoboidentities {
id?: number;
category: 'roboidentities';
type: 'roboname' | 'robohash';
string?: string;
size?: string;
}
export declare type NativeWebViewMessage =
| NativeWebViewMessageHttp
| NativeWebViewMessageSystem
| NativeWebViewMessageRoboidentities
| NA;
export interface NativeRobosatsPromise {
resolve: (value: object | PromiseLike<object>) => void;

View File

@ -0,0 +1,4 @@
import RoboidentitiesClientNativeClient from './RoboidentitiesNativeClient';
import { RoboidentitiesClient } from './type';
export const roboidentitiesClient: RoboidentitiesClient = new RoboidentitiesClientNativeClient();

View File

@ -0,0 +1,42 @@
import { type RoboidentitiesClient } from '../type';
class RoboidentitiesNativeClient implements RoboidentitiesClient {
private robonames: Record<string, string> = {};
private robohashes: Record<string, string> = {};
public generateRoboname: (initialString: string) => Promise<string> = async (initialString) => {
if (this.robonames[initialString]) {
return this.robonames[initialString];
} else {
const response = await window.NativeRobosats?.postMessage({
category: 'roboidentities',
type: 'roboname',
detail: initialString,
});
const result = response ? Object.values(response)[0] : '';
this.robonames[initialString] = result;
return result;
}
};
public generateRobohash: (initialString: string, size: 'small' | 'large') => Promise<string> =
async (initialString, size) => {
const key = `${initialString};${size === 'small' ? 80 : 256}`;
if (this.robohashes[key]) {
return this.robohashes[key];
} else {
const response = await window.NativeRobosats?.postMessage({
category: 'roboidentities',
type: 'robohash',
detail: key,
});
const result = response ? Object.values(response)[0] : '';
const image = `data:image/png;base64,${result}`;
this.robohashes[key] = image;
return image;
}
};
}
export default RoboidentitiesNativeClient;

View File

@ -81,7 +81,7 @@ class RoboGenerator {
hash,
size,
) => {
const cacheKey = `${size}px;${hash}`;
const cacheKey = `${hash};${size}`;
if (this.assetsCache[cacheKey]) {
return this.assetsCache[cacheKey];
} else {

View File

@ -0,0 +1,18 @@
import { type RoboidentitiesClient } from '../type';
import { generate_roboname } from 'robo-identities-wasm';
import { robohash } from './RobohashGenerator';
class RoboidentitiesClientWebClient implements RoboidentitiesClient {
public generateRoboname: (initialString: string) => Promise<string> = async (initialString) => {
return new Promise<string>(async (resolve, _reject) => {
resolve(generate_roboname(initialString));
});
};
public generateRobohash: (initialString: string, size: 'small' | 'large') => Promise<string> =
async (initialString, size) => {
return robohash.generate(initialString, size);
};
}
export default RoboidentitiesClientWebClient;

View File

@ -0,0 +1,4 @@
import RoboidentitiesClientWebClient from './RoboidentitiesWebClient';
import { RoboidentitiesClient } from './type';
export const roboidentitiesClient: RoboidentitiesClient = new RoboidentitiesClientWebClient();

View File

@ -0,0 +1,4 @@
export interface RoboidentitiesClient {
generateRoboname: (initialString: string) => Promise<string>;
generateRobohash: (initialString: string, size: 'small' | 'large') => Promise<string>;
}

View File

@ -89,41 +89,6 @@ class ApiNativeClient implements ApiClient {
headers: this.getHeaders(auth),
}).then(this.parseResponse);
};
public fileImageUrl: (baseUrl: string, path: string) => Promise<string | undefined> = async (
baseUrl,
path,
) => {
if (path === '') {
return await Promise.resolve('');
}
if (this.assetsCache[path] != null) {
return await Promise.resolve(this.assetsCache[path]);
} else if (this.assetsPromises.has(path)) {
return await this.assetsPromises.get(path);
}
this.assetsPromises.set(
path,
new Promise<string>((resolve, reject) => {
window.NativeRobosats?.postMessage({
category: 'http',
type: 'xhr',
baseUrl,
path,
})
.then((fileB64: { b64Data: string }) => {
this.assetsCache[path] = `data:image/png;base64,${fileB64.b64Data}`;
this.assetsPromises.delete(path);
resolve(this.assetsCache[path]);
})
.catch(reject);
}),
);
return await this.assetsPromises.get(path);
};
}
export default ApiNativeClient;

View File

@ -11,7 +11,6 @@ export interface ApiClient {
put: (baseUrl: string, path: string, body: object, auth?: Auth) => Promise<object | undefined>;
get: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined>;
delete: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined>;
fileImageUrl?: (baseUrl: string, path: string) => Promise<string | undefined>;
}
export const apiClient: ApiClient =

View File

@ -56,6 +56,15 @@ const configMobile: Configuration = {
async: true,
},
},
{
test: path.resolve(__dirname, 'src/services/Roboidentities/Web.ts'),
loader: 'file-replace-loader',
options: {
condition: 'if-replacement-exists',
replacement: path.resolve(__dirname, 'src/services/Roboidentities/Native.ts'),
async: true,
},
},
{
test: path.resolve(__dirname, 'src/components/RobotAvatar/placeholder.json'),
loader: 'file-replace-loader',
@ -81,6 +90,10 @@ const configMobile: Configuration = {
from: path.resolve(__dirname, 'static/assets/sounds'),
to: path.resolve(__dirname, '../mobile/html/Web.bundle/assets/sounds'),
},
{
from: path.resolve(__dirname, 'static/federation'),
to: path.resolve(__dirname, '../mobile/html/Web.bundle/assets/federation'),
},
],
}),
],

View File

@ -1,23 +1,45 @@
import React, { useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import { SafeAreaView, Text, Platform, Appearance } from 'react-native';
import { SafeAreaView, Text, Platform, Appearance, DeviceEventEmitter } from 'react-native';
import TorClient from './services/Tor';
import Clipboard from '@react-native-clipboard/clipboard';
import NetInfo from '@react-native-community/netinfo';
import EncryptedStorage from 'react-native-encrypted-storage';
import { name as app_name, version as app_version } from './package.json';
import TorModule from './native/TorModule';
import RoboIdentitiesModule from './native/RoboIdentitiesModule';
const backgroundColors = {
light: 'white',
dark: 'black',
};
export type TorStatus = 'ON' | 'STARTING' | 'STOPPING' | 'OFF';
const App = () => {
const colorScheme = Appearance.getColorScheme() ?? 'light';
const torClient = new TorClient();
const webViewRef = useRef<WebView>();
const uri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/index.html';
useEffect(() => {
TorModule.start();
DeviceEventEmitter.addListener('TorStatus', (payload) => {
if (payload.torStatus === 'OFF') TorModule.restart();
injectMessage({
category: 'system',
type: 'torStatus',
detail: payload.torStatus,
});
});
}, []);
useEffect(() => {
const interval = setInterval(() => {
TorModule.getTorStatus();
}, 2000);
return () => clearInterval(interval);
}, []);
const injectMessageResolve = (id: string, data?: object) => {
const json = JSON.stringify(data || {});
webViewRef.current?.injectJavaScript(
@ -72,7 +94,7 @@ const App = () => {
const onMessage = async (event: WebViewMessageEvent) => {
const data = JSON.parse(event.nativeEvent.data);
if (data.category === 'http') {
sendTorStatus();
TorModule.getTorStatus();
if (data.type === 'get') {
torClient
.get(data.baseUrl, data.path, data.headers)
@ -80,7 +102,7 @@ const App = () => {
injectMessageResolve(data.id, response);
})
.catch((e) => onCatch(data.id, e))
.finally(sendTorStatus);
.finally(TorModule.getTorStatus);
} else if (data.type === 'post') {
torClient
.post(data.baseUrl, data.path, data.body, data.headers)
@ -88,7 +110,7 @@ const App = () => {
injectMessageResolve(data.id, response);
})
.catch((e) => onCatch(data.id, e))
.finally(sendTorStatus);
.finally(TorModule.getTorStatus);
} else if (data.type === 'delete') {
torClient
.delete(data.baseUrl, data.path, data.headers)
@ -96,15 +118,7 @@ const App = () => {
injectMessageResolve(data.id, response);
})
.catch((e) => onCatch(data.id, e))
.finally(sendTorStatus);
} else if (data.type === 'xhr') {
torClient
.request(data.baseUrl, data.path)
.then((response: object) => {
injectMessageResolve(data.id, response);
})
.catch((e) => onCatch(data.id, e))
.finally(sendTorStatus);
.finally(TorModule.getTorStatus);
}
} else if (data.category === 'system') {
if (data.type === 'init') {
@ -116,6 +130,14 @@ const App = () => {
} else if (data.type === 'deleteCookie') {
EncryptedStorage.removeItem(data.key);
}
} else if (data.category === 'roboidentities') {
if (data.type === 'roboname') {
const roboname = await RoboIdentitiesModule.generateRoboname(data.detail);
injectMessageResolve(data.id, { roboname });
} else if (data.type === 'robohash') {
const robohash = await RoboIdentitiesModule.generateRobohash(data.detail);
injectMessageResolve(data.id, { robohash });
}
}
};
@ -132,23 +154,6 @@ const App = () => {
} catch (error) {}
};
const sendTorStatus = async (event?: any) => {
NetInfo.fetch().then(async (state) => {
let daemonStatus = 'ERROR';
if (state.isInternetReachable) {
try {
daemonStatus = await torClient.daemon.getDaemonStatus();
} catch {}
}
injectMessage({
category: 'system',
type: 'torStatus',
detail: daemonStatus,
});
});
};
return (
<SafeAreaView style={{ flex: 1, backgroundColor: backgroundColors[colorScheme] }}>
<WebView

View File

@ -271,6 +271,7 @@ android {
packagingOptions {
// Make sure libjsc.so does not packed in APK
exclude "**/libjsc.so"
jniLibs.useLegacyPackaging = true
}
}
@ -282,7 +283,9 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
implementation files("../../node_modules/react-native-tor/android/libs/sifir_android.aar")
implementation "io.matthewnelson.kotlin-components:kmp-tor:4.8.6-0-1.4.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
if (enableHermes) {
//noinspection GradleDynamicVersion
@ -326,3 +329,5 @@ def isNewArchitectureEnabled() {
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
}
apply plugin: 'kotlin-android'

View File

@ -10,7 +10,9 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:usesCleartextTraffic="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:extractNativeLibs="true"
>
<activity
android:name=".MainActivity"
android:label="@string/app_name"

View File

@ -1,17 +1,14 @@
package com.robosats;
import android.app.Application;
import android.content.Context;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.soloader.SoLoader;
import android.webkit.WebView;
import com.robosats.newarchitecture.MainApplicationReactNativeHost;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
public class MainApplication extends Application implements ReactApplication {
@ -29,6 +26,8 @@ public class MainApplication extends Application implements ReactApplication {
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new RobosatsPackage());
return packages;
}

View File

@ -0,0 +1,22 @@
package com.robosats;
import android.util.Log;
public class RoboIdentities {
static {
System.loadLibrary("robonames");
System.loadLibrary("robohash");
}
public String generateRoboname(String initial_string) {
return nativeGenerateRoboname(initial_string);
}
public String generateRobohash(String initial_string) {
return nativeGenerateRobohash(initial_string);
}
// Native functions implemented in Rust.
private static native String nativeGenerateRoboname(String initial_string);
private static native String nativeGenerateRobohash(String initial_string);
}

View File

@ -0,0 +1,30 @@
package com.robosats;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.robosats.modules.RoboIdentitiesModule;
import com.robosats.modules.TorModule;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class RobosatsPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new TorModule(reactContext));
modules.add(new RoboIdentitiesModule(reactContext));
return modules;
}
}

View File

@ -0,0 +1,37 @@
package com.robosats.modules;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.robosats.RoboIdentities;
public class RoboIdentitiesModule extends ReactContextBaseJavaModule {
private ReactApplicationContext context;
public RoboIdentitiesModule(ReactApplicationContext reactContext) {
context = reactContext;
}
@Override
public String getName() {
return "RoboIdentitiesModule";
}
@ReactMethod
public void generateRoboname(String initial_string, final Promise promise) {
String roboname = new RoboIdentities().generateRoboname(initial_string);
promise.resolve(roboname);
}
@ReactMethod
public void generateRobohash(String initial_string, final Promise promise) {
String robohash = new RoboIdentities().generateRobohash(initial_string);
promise.resolve(robohash);
}
}

View File

@ -0,0 +1,162 @@
package com.robosats.modules;
import android.util.Log;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.robosats.tor.TorKmpManager;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class TorModule extends ReactContextBaseJavaModule {
private TorKmpManager torKmpManager;
private ReactApplicationContext context;
public TorModule(ReactApplicationContext reactContext) {
context = reactContext;
}
@Override
public String getName() {
return "TorModule";
}
@ReactMethod
public void sendRequest(String action, String url, String headers, String body, final Promise promise) throws JSONException {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout
.readTimeout(30, TimeUnit.SECONDS) // Set read timeout
.proxy(torKmpManager.getProxy()).build();
Request.Builder requestBuilder = new Request.Builder().url(url);
JSONObject headersObject = new JSONObject(headers);
headersObject.keys().forEachRemaining(key -> {
String value = headersObject.optString(key);
requestBuilder.addHeader(key, value);
});
if (Objects.equals(action, "DELETE")) {
requestBuilder.delete();
} else if (Objects.equals(action, "POST")) {
RequestBody requestBody = RequestBody.create(body, MediaType.get("application/json; charset=utf-8"));
requestBuilder.post(requestBody);
} else {
requestBuilder.get();
}
Request request = requestBuilder.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.d("RobosatsError", e.toString());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String body = response.body() != null ? response.body().string() : "{}";
JSONObject headersJson = new JSONObject();
response.headers().names().forEach(name -> {
try {
headersJson.put(name, response.header(name));
} catch (JSONException e) {
throw new RuntimeException(e);
}
});
if (response.code() != 200 && response.code() != 201) {
Log.d("RobosatsError", "Request error code: " + response.code());
} else if (response.isSuccessful()) {
promise.resolve("{\"json\":" + body + ", \"headers\": " + headersJson +"}");
}
}
});
}
@ReactMethod
public void getTorStatus() {
String torState = torKmpManager.getTorState().getState().name();
WritableMap payload = Arguments.createMap();
payload.putString("torStatus", torState);
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("TorStatus", payload);
}
@ReactMethod
public void isConnected() {
String isConnected = String.valueOf(torKmpManager.isConnected());
WritableMap payload = Arguments.createMap();
payload.putString("isConnected", isConnected);
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("TorIsConnected", payload);
}
@ReactMethod
public void isStarting() {
String isStarting = String.valueOf(torKmpManager.isStarting());
WritableMap payload = Arguments.createMap();
payload.putString("isStarting", isStarting);
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("TorIsStarting", payload);
}
@ReactMethod
public void stop() {
torKmpManager.getTorOperationManager().stopQuietly();
WritableMap payload = Arguments.createMap();
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("TorStop", payload);
}
@ReactMethod
public void start() {
torKmpManager = new TorKmpManager(context.getCurrentActivity().getApplication());
torKmpManager.getTorOperationManager().startQuietly();
WritableMap payload = Arguments.createMap();
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("TorStart", payload);
}
@ReactMethod
public void restart() {
torKmpManager = new TorKmpManager(context.getCurrentActivity().getApplication());
torKmpManager.getTorOperationManager().restartQuietly();
WritableMap payload = Arguments.createMap();
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("TorRestart", payload);
}
@ReactMethod
public void newIdentity() {
torKmpManager.newIdentity(context.getCurrentActivity().getApplication());
WritableMap payload = Arguments.createMap();
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("TorNewIdentity", payload);
}
}

View File

@ -0,0 +1,8 @@
package com.robosats.tor
enum class EnumTorState {
STARTING,
ON,
STOPPING,
OFF
}

View File

@ -0,0 +1,389 @@
package com.robosats.tor
import android.app.Application
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.matthewnelson.kmp.tor.KmpTorLoaderAndroid
import io.matthewnelson.kmp.tor.TorConfigProviderAndroid
import io.matthewnelson.kmp.tor.common.address.*
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Option.*
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Setting.*
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlInfoGet
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal
import io.matthewnelson.kmp.tor.controller.common.events.TorEvent
import io.matthewnelson.kmp.tor.manager.TorManager
import io.matthewnelson.kmp.tor.manager.TorServiceConfig
import io.matthewnelson.kmp.tor.manager.common.TorControlManager
import io.matthewnelson.kmp.tor.manager.common.TorOperationManager
import io.matthewnelson.kmp.tor.manager.common.event.TorManagerEvent
import io.matthewnelson.kmp.tor.manager.common.state.isOff
import io.matthewnelson.kmp.tor.manager.common.state.isOn
import io.matthewnelson.kmp.tor.manager.common.state.isStarting
import io.matthewnelson.kmp.tor.manager.common.state.isStopping
import io.matthewnelson.kmp.tor.manager.R
import kotlinx.coroutines.*
import java.net.InetSocketAddress
import java.net.Proxy
class TorKmpManager(application : Application) {
private val TAG = "TorListener"
private val providerAndroid by lazy {
object : TorConfigProviderAndroid(context = application) {
override fun provide(): TorConfig {
return TorConfig.Builder {
// Set multiple ports for all of the things
val dns = Ports.Dns()
put(dns.set(AorDorPort.Value(PortProxy(9252))))
put(dns.set(AorDorPort.Value(PortProxy(9253))))
val socks = Ports.Socks()
put(socks.set(AorDorPort.Value(PortProxy(9254))))
put(socks.set(AorDorPort.Value(PortProxy(9255))))
val http = Ports.HttpTunnel()
put(http.set(AorDorPort.Value(PortProxy(9258))))
put(http.set(AorDorPort.Value(PortProxy(9259))))
val trans = Ports.Trans()
put(trans.set(AorDorPort.Value(PortProxy(9262))))
put(trans.set(AorDorPort.Value(PortProxy(9263))))
// If a port (9263) is already taken (by ^^^^ trans port above)
// this will take its place and "overwrite" the trans port entry
// because port 9263 is taken.
put(socks.set(AorDorPort.Value(PortProxy(9263))))
// Set Flags
socks.setFlags(setOf(
Ports.Socks.Flag.OnionTrafficOnly
)).setIsolationFlags(setOf(
Ports.IsolationFlag.IsolateClientAddr,
)).set(AorDorPort.Value(PortProxy(9264)))
put(socks)
// reset our socks object to defaults
socks.setDefault()
// Not necessary, as if ControlPort is missing it will be
// automatically added for you; but for demonstration purposes...
// put(Ports.Control().set(AorDorPort.Auto))
// Use a UnixSocket instead of TCP for the ControlPort.
//
// A unix domain socket will always be preferred on Android
// if neither Ports.Control or UnixSockets.Control are provided.
put(UnixSockets.Control().set(FileSystemFile(
workDir.builder {
// Put the file in the "data" directory
// so that we avoid any directory permission
// issues.
//
// Note that DataDirectory is automatically added
// for you if it is not present in your provided
// config. If you set a custom Path for it, you
// should use it here.
addSegment(DataDirectory.DEFAULT_NAME)
addSegment(UnixSockets.Control.DEFAULT_NAME)
}
)))
// Use a UnixSocket instead of TCP for the SocksPort.
put(UnixSockets.Socks().set(FileSystemFile(
workDir.builder {
// Put the file in the "data" directory
// so that we avoid any directory permission
// issues.
//
// Note that DataDirectory is automatically added
// for you if it is not present in your provided
// config. If you set a custom Path for it, you
// should use it here.
addSegment(DataDirectory.DEFAULT_NAME)
addSegment(UnixSockets.Socks.DEFAULT_NAME)
}
)))
// For Android, disabling & reducing connection padding is
// advisable to minimize mobile data usage.
put(ConnectionPadding().set(AorTorF.False))
put(ConnectionPaddingReduced().set(TorF.True))
// Tor default is 24h. Reducing to 10 min helps mitigate
// unnecessary mobile data usage.
put(DormantClientTimeout().set(Time.Minutes(10)))
// Tor defaults this setting to false which would mean if
// Tor goes dormant, the next time it is started it will still
// be in the dormant state and will not bootstrap until being
// set to "active". This ensures that if it is a fresh start,
// dormancy will be cancelled automatically.
put(DormantCanceledByStartup().set(TorF.True))
// If planning to use v3 Client Authentication in a persistent
// manner (where private keys are saved to disk via the "Persist"
// flag), this is needed to be set.
put(ClientOnionAuthDir().set(FileSystemDir(
workDir.builder { addSegment(ClientOnionAuthDir.DEFAULT_NAME) }
)))
val hsPath = workDir.builder {
addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME)
addSegment("test_service")
}
// Add Hidden services
put(HiddenService()
.setPorts(ports = setOf(
// Use a unix domain socket to communicate via IPC instead of over TCP
HiddenService.UnixSocket(virtualPort = Port(80), targetUnixSocket = hsPath.builder {
addSegment(HiddenService.UnixSocket.DEFAULT_UNIX_SOCKET_NAME)
}),
))
.setMaxStreams(maxStreams = HiddenService.MaxStreams(value = 2))
.setMaxStreamsCloseCircuit(value = TorF.True)
.set(FileSystemDir(path = hsPath))
)
put(HiddenService()
.setPorts(ports = setOf(
HiddenService.Ports(virtualPort = Port(80), targetPort = Port(1030)), // http
HiddenService.Ports(virtualPort = Port(443), targetPort = Port(1030)) // https
))
.set(FileSystemDir(path =
workDir.builder {
addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME)
addSegment("test_service_2")
}
))
)
}.build()
}
}
}
private val loaderAndroid by lazy {
KmpTorLoaderAndroid(provider = providerAndroid)
}
private val manager: TorManager by lazy {
TorManager.newInstance(application = application, loader = loaderAndroid, requiredEvents = null)
}
// only expose necessary interfaces
val torOperationManager: TorOperationManager get() = manager
val torControlManager: TorControlManager get() = manager
private val listener = TorListener()
val events: LiveData<String> get() = listener.eventLines
private val appScope by lazy {
CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
}
val torStateLiveData: MutableLiveData<TorState> = MutableLiveData()
get() = field
var torState: TorState = TorState()
get() = field
var proxy: Proxy? = null
get() = field
init {
manager.debug(true)
manager.addListener(listener)
listener.addLine(TorServiceConfig.getMetaData(application).toString())
}
fun isConnected(): Boolean {
return manager.state.isOn() && manager.state.bootstrap >= 100
}
fun isStarting(): Boolean {
return manager.state.isStarting() ||
(manager.state.isOn() && manager.state.bootstrap < 100);
}
fun newIdentity(appContext: Application) {
appScope.launch {
val result = manager.signal(TorControlSignal.Signal.NewNym)
result.onSuccess {
if (it !is String) {
listener.addLine(TorControlSignal.NEW_NYM_SUCCESS)
Toast.makeText(appContext, TorControlSignal.NEW_NYM_SUCCESS, Toast.LENGTH_SHORT).show()
return@onSuccess
}
val post: String? = when {
it.startsWith(TorControlSignal.NEW_NYM_RATE_LIMITED) -> {
// Rate limiting NEWNYM request: delaying by 8 second(s)
val seconds: Int? = it.drop(TorControlSignal.NEW_NYM_RATE_LIMITED.length)
.substringBefore(' ')
.toIntOrNull()
if (seconds == null) {
it
} else {
appContext.getString(
R.string.kmp_tor_newnym_rate_limited,
seconds
)
}
}
it == TorControlSignal.NEW_NYM_SUCCESS -> {
appContext.getString(R.string.kmp_tor_newnym_success)
}
else -> {
null
}
}
if (post != null) {
listener.addLine(post)
Toast.makeText(appContext, post, Toast.LENGTH_SHORT).show()
}
}
result.onFailure {
val msg = "Tor identity change failed"
listener.addLine(msg)
Toast.makeText(appContext, msg, Toast.LENGTH_SHORT).show()
}
}
}
private inner class TorListener: TorManagerEvent.Listener() {
private val _eventLines: MutableLiveData<String> = MutableLiveData("")
val eventLines: LiveData<String> = _eventLines
private val events: MutableList<String> = ArrayList(50)
fun addLine(line: String) {
synchronized(this) {
if (events.size > 49) {
events.removeAt(0)
}
events.add(line)
//Log.i(TAG, line)
//_eventLines.value = events.joinToString("\n")
_eventLines.postValue(events.joinToString("\n"))
}
}
override fun onEvent(event: TorManagerEvent) {
if (event is TorManagerEvent.State) {
val stateEvent: TorManagerEvent.State = event
val state = stateEvent.torState
torState.progressIndicator = state.bootstrap
val liveTorState = TorState()
liveTorState.progressIndicator = state.bootstrap
if (state.isOn()) {
if (state.bootstrap >= 100) {
torState.state = EnumTorState.ON
liveTorState.state = EnumTorState.ON
} else {
torState.state = EnumTorState.STARTING
liveTorState.state = EnumTorState.STARTING
}
} else if (state.isStarting()) {
torState.state = EnumTorState.STARTING
liveTorState.state = EnumTorState.STARTING
} else if (state.isOff()) {
torState.state = EnumTorState.OFF
liveTorState.state = EnumTorState.OFF
} else if (state.isStopping()) {
torState.state = EnumTorState.STOPPING
liveTorState.state = EnumTorState.STOPPING
}
torStateLiveData.postValue(liveTorState)
}
addLine(event.toString())
super.onEvent(event)
}
override fun onEvent(event: TorEvent.Type.SingleLineEvent, output: String) {
addLine("$event - $output")
super.onEvent(event, output)
}
override fun onEvent(event: TorEvent.Type.MultiLineEvent, output: List<String>) {
addLine("multi-line event: $event. See Logs.")
// these events are many many many lines and should be moved
// off the main thread if ever needed to be dealt with.
val enabled = false
if (enabled) {
appScope.launch(Dispatchers.IO) {
Log.d(TAG, "-------------- multi-line event START: $event --------------")
for (line in output) {
Log.d(TAG, line)
}
Log.d(TAG, "--------------- multi-line event END: $event ---------------")
}
}
super.onEvent(event, output)
}
override fun managerEventError(t: Throwable) {
t.printStackTrace()
}
override fun managerEventAddressInfo(info: TorManagerEvent.AddressInfo) {
if (info.isNull) {
// Tear down HttpClient
} else {
info.socksInfoToProxyAddressOrNull()?.firstOrNull()?.let { proxyAddress ->
@Suppress("UNUSED_VARIABLE")
val socket = InetSocketAddress(proxyAddress.address.value, proxyAddress.port.value)
proxy = Proxy(Proxy.Type.SOCKS, socket)
}
}
}
override fun managerEventStartUpCompleteForTorInstance() {
// Do one-time things after we're bootstrapped
appScope.launch {
torControlManager.onionAddNew(
type = OnionAddress.PrivateKey.Type.ED25519_V3,
hsPorts = setOf(HiddenService.Ports(virtualPort = Port(443))),
flags = null,
maxStreams = null,
).onSuccess { hsEntry ->
addLine(
"New HiddenService: " +
"\n - Address: https://${hsEntry.address.canonicalHostname()}" +
"\n - PrivateKey: ${hsEntry.privateKey}"
)
torControlManager.onionDel(hsEntry.address).onSuccess {
addLine("Aaaaaaaaand it's gone...")
}.onFailure { t ->
t.printStackTrace()
}
}.onFailure { t ->
t.printStackTrace()
}
delay(20_000L)
torControlManager.infoGet(TorControlInfoGet.KeyWord.Uptime()).onSuccess { uptime ->
addLine("Uptime - $uptime")
}.onFailure { t ->
t.printStackTrace()
}
}
}
}
}

View File

@ -0,0 +1,14 @@
package com.robosats.tor
class TorState {
var state : EnumTorState = EnumTorState.OFF
get() = field
set(value) {
field = value
}
var progressIndicator : Int = 0
get() = field
set(value) {
field = value
}
}

Binary file not shown.

View File

@ -9,7 +9,6 @@ buildscript {
compileSdkVersion = 33
targetSdkVersion = 33
kotlin_version = "1.8.21"
kotlinVersion = "1.8.21" //for react-native-tor
if (System.properties['os.arch'] == "aarch64") {
// For M1 Users we need to use the NDK 24 which added support for aarch64

View File

@ -0,0 +1,9 @@
import { NativeModules } from 'react-native';
const { RoboIdentitiesModule } = NativeModules;
interface RoboIdentitiesModuleInterface {
generateRoboname: (initialString: String) => Promise<string>;
generateRobohash: (initialString: String) => Promise<string>;
}
export default RoboIdentitiesModule as RoboIdentitiesModuleInterface;

View File

@ -0,0 +1,11 @@
import { NativeModules } from 'react-native';
const { TorModule } = NativeModules;
interface TorModuleInterface {
start: () => void;
restart: () => void;
getTorStatus: () => void;
sendRequest: (action: string, url: string, headers: string, body: string) => Promise<string>;
}
export default TorModule as TorModuleInterface;

View File

@ -13,7 +13,6 @@
"react": "18.2.0",
"react-native": "^0.71.8",
"react-native-encrypted-storage": "^4.0.3",
"react-native-tor": "^0.1.8",
"react-native-webview": "^13.3.0"
},
"devDependencies": {
@ -4330,10 +4329,6 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@types/async": {
"version": "3.2.20",
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz",
@ -12972,18 +12967,6 @@
"version": "0.71.18",
"license": "MIT"
},
"node_modules/react-native-tor": {
"version": "0.1.8",
"license": "MIT",
"dependencies": {
"@types/async": "^3.2.6",
"async": "^3.2.0"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-webview": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.3.0.tgz",

View File

@ -17,7 +17,6 @@
"react": "18.2.0",
"react-native": "^0.71.8",
"react-native-encrypted-storage": "^4.0.3",
"react-native-tor": "^0.1.8",
"react-native-webview": "^13.3.0"
},
"devDependencies": {

View File

@ -1,29 +1,6 @@
import Tor from 'react-native-tor';
import TorModule from '../../native/TorModule';
class TorClient {
daemon: ReturnType<typeof Tor>;
constructor() {
this.daemon = Tor({
stopDaemonOnBackground: false,
numberConcurrentRequests: 0,
});
}
private readonly connectDaemon: () => void = async () => {
try {
this.daemon.startIfNotStarted();
} catch {
console.log('TOR already started');
}
};
public reset: () => void = async () => {
console.log('Reset TOR');
await this.daemon.stopIfRunning();
await this.daemon.startIfNotStarted();
};
public get: (baseUrl: string, path: string, headers: object) => Promise<object> = async (
baseUrl,
path,
@ -31,9 +8,13 @@ class TorClient {
) => {
return await new Promise<object>(async (resolve, reject) => {
try {
const response = await this.daemon.get(`${baseUrl}${path}`, headers);
resolve(response);
const response = await TorModule.sendRequest(
'GET',
`${baseUrl}${path}`,
JSON.stringify(headers),
'{}',
);
resolve(JSON.parse(response));
} catch (error) {
reject(error);
}
@ -47,28 +28,13 @@ class TorClient {
) => {
return await new Promise<object>(async (resolve, reject) => {
try {
const response = await this.daemon.delete(`${baseUrl}${path}`, '', headers);
resolve(response);
} catch (error) {
reject(error);
}
});
};
public request: (baseUrl: string, path: string) => Promise<object> = async (
baseUrl: string,
path,
) => {
return await new Promise<object>(async (resolve, reject) => {
try {
const response = await this.daemon
.request(`${baseUrl}${path}`, 'GET', '', {}, true)
.then((resp) => {
resolve(resp);
});
resolve(response);
const response = await TorModule.sendRequest(
'DELETE',
`${baseUrl}${path}`,
JSON.stringify(headers),
'{}',
);
resolve(JSON.parse(response));
} catch (error) {
reject(error);
}
@ -80,9 +46,13 @@ class TorClient {
return await new Promise<object>(async (resolve, reject) => {
try {
const json = JSON.stringify(body);
const response = await this.daemon.post(`${baseUrl}${path}`, json, headers);
resolve(response);
const response = await TorModule.sendRequest(
'POST',
`${baseUrl}${path}`,
JSON.stringify(headers),
json,
);
resolve(JSON.parse(response));
} catch (error) {
reject(error);
}