Skip to content

Commit

Permalink
Merge pull request #84 from signum-network/feat/78-use-public-resourc…
Browse files Browse the repository at this point in the history
…e-nodes

feat: loading public network resources...and checking for reachability
  • Loading branch information
ohager authored Jul 20, 2024
2 parents 8b5fe07 + c5ab40d commit ac6f9ae
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 185 deletions.
2 changes: 1 addition & 1 deletion src/app/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { ReactComponent as SettingsIcon } from 'app/icons/settings.svg';
import { ReactComponent as SignalAltIcon } from 'app/icons/signal-alt.svg';
import PageLayout from 'app/layouts/PageLayout';
import About from 'app/templates/Settings/About';
import CustomNetworksSettings from 'app/templates/Settings/CustomNetworksSettings';
import DAppSettings from 'app/templates/Settings/DAppSettings';
import GeneralSettings from 'app/templates/Settings/GeneralSettings';
import HelpAndCommunity from 'app/templates/Settings/HelpAndCommunity';
import CustomNetworksSettings from 'app/templates/Settings/NetworkSettings';
import NostrAccount from 'app/templates/Settings/NostrAccount';
import NostrRelaysSettings from 'app/templates/Settings/NostrRelaysSettings';
import RemoveAccount from 'app/templates/Settings/RemoveAccount';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@ import React, { FC, useCallback } from 'react';
import classNames from 'clsx';
import { useForm } from 'react-hook-form';

import FormCheckbox from 'app/atoms/FormCheckbox';
import FormField from 'app/atoms/FormField';
import FormSubmitButton from 'app/atoms/FormSubmitButton';
import Name from 'app/atoms/Name';
import NetworkBadge from 'app/atoms/NetworkBadge';
import SubTitle from 'app/atoms/SubTitle';
import { HTTP_URL_PATTERN } from 'app/defaults';
import { ReactComponent as CloseIcon } from 'app/icons/close.svg';
import { T, t } from 'lib/i18n/react';
import { Network, useSettings, useTempleClient, canConnectToNetwork, NetworkName } from 'lib/temple/front';
import { canConnectToNetwork, NetworkName, useSettings, useTempleClient } from 'lib/temple/front';
import { COLORS } from 'lib/ui/colors';
import { useConfirm } from 'lib/ui/dialog';
import { withErrorHumanDelay } from 'lib/ui/humanDelay';

import FormCheckbox from '../../atoms/FormCheckbox';
import { NetworksListItem } from './NetworksListItem';

interface NetworkFormData {
name: string;
Expand Down Expand Up @@ -58,7 +56,7 @@ const CustomNetworksSettings: FC = () => {
if (submitting) return;
clearError();
const type = isTestnet ? 'test' : 'main';
const canConnect = await canConnectToNetwork(rpcBaseURL, type);
const canConnect = await canConnectToNetwork(rpcBaseURL);
if (!canConnect) {
await withErrorHumanDelay(`cannot connect to ${rpcBaseURL}`, () =>
setError('rpcBaseURL', SUBMIT_ERROR_TYPE, t('cantConnectToNetwork'))
Expand Down Expand Up @@ -120,17 +118,10 @@ const CustomNetworksSettings: FC = () => {
<div className="w-full max-w-sm p-2 pb-4 mx-auto">
<div className="flex flex-col mb-8">
<h2 className={classNames('mb-4', 'leading-tight', 'flex flex-col')}>
<T id="currentNetworks">
{message => <span className="text-base font-semibold text-gray-700">{message}</span>}
</T>

<T id="deleteNetworkHint">
{message => (
<span className={classNames('mt-1', 'text-xs font-light text-gray-600')} style={{ maxWidth: '90%' }}>
{message}
</span>
)}
</T>
<span className="text-base font-semibold text-gray-700">{t('currentNetworks')}</span>
<span className={classNames('mt-1', 'text-xs font-light text-gray-600')} style={{ maxWidth: '90%' }}>
{t('deleteNetworkHint')}
</span>
</h2>

<div
Expand Down Expand Up @@ -213,74 +204,3 @@ const CustomNetworksSettings: FC = () => {
};

export default CustomNetworksSettings;

type NetworksListItemProps = {
canRemove: boolean;
network: Network;
onRemoveClick?: (baseUrl: string) => void;
last: boolean;
};

const NetworksListItem: FC<NetworksListItemProps> = props => {
const {
network: { name, nameI18nKey, rpcBaseURL, color, networkName },
canRemove,
onRemoveClick,
last
} = props;
const handleRemoveClick = useCallback(() => onRemoveClick?.(rpcBaseURL), [onRemoveClick, rpcBaseURL]);

return (
<div
className={classNames(
'block w-full',
'overflow-hidden',
!last && 'border-b border-gray-200',
'flex items-stretch',
'text-gray-700',
'transition ease-in-out duration-200',
'focus:outline-none',
'opacity-90 hover:opacity-100'
)}
style={{
padding: '0.4rem 0.375rem 0.4rem 0.375rem'
}}
>
<div
className={classNames('mt-1 ml-2 mr-3', 'w-3 h-3', 'border border-primary-white', 'rounded-full shadow-xs')}
style={{ background: color }}
/>

<div className="flex flex-col justify-between flex-1">
<div className="flex flex-row justify-between">
<Name className="mb-1 text-sm font-medium leading-tight">
{(nameI18nKey && <T id={nameI18nKey} />) || name}
</Name>
<NetworkBadge networkName={networkName} />
</div>

<div
className={classNames('text-xs text-gray-700 font-light', 'flex items-center')}
style={{
marginBottom: '0.125rem'
}}
>
URL:<Name className="ml-1 font-normal">{rpcBaseURL}</Name>
</div>
</div>

{canRemove && (
<button
className={classNames(
'flex-none p-2',
'text-gray-500 hover:text-gray-600',
'transition ease-in-out duration-200'
)}
onClick={handleRemoveClick}
>
<CloseIcon className="w-auto h-5 stroke-current stroke-2" title={t('delete')} />
</button>
)}
</div>
);
};
89 changes: 89 additions & 0 deletions src/app/templates/Settings/NetworkSettings/NetworksListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { FC, useCallback } from 'react';

import classNames from 'clsx';
import useSWR from 'swr';

import Name from 'app/atoms/Name';
import NetworkBadge from 'app/atoms/NetworkBadge';
import { ReactComponent as CloseIcon } from 'app/icons/close.svg';
import { t, T } from 'lib/i18n/react';
import { Network } from 'lib/messaging';
import { canConnectToNetwork } from 'lib/temple/front';

type NetworksListItemProps = {
canRemove: boolean;
network: Network;
onRemoveClick?: (baseUrl: string) => void;
last: boolean;
};

export const NetworksListItem: FC<NetworksListItemProps> = props => {
const {
network: { name, nameI18nKey, rpcBaseURL, color, networkName },
canRemove,
onRemoveClick,
last
} = props;
const handleRemoveClick = useCallback(() => onRemoveClick?.(rpcBaseURL), [onRemoveClick, rpcBaseURL]);

const { data: isReachable, isValidating } = useSWR([rpcBaseURL, 5_000, 'canConnectToNetwork'], canConnectToNetwork, {
refreshInterval: 60_000,
shouldRetryOnError: false,
revalidateOnFocus: false
});

return (
<div
className={classNames(
'block w-full',
'overflow-hidden',
!last && 'border-b border-gray-200',
'flex items-stretch',
'text-gray-700',
'transition ease-in-out duration-200',
'focus:outline-none',
isValidating && 'animate-pulse',
!isValidating && !isReachable && 'opacity-25 bg-red-300'
)}
style={{
padding: '0.4rem 0.375rem 0.4rem 0.375rem'
}}
>
<div
className={classNames('mt-1 ml-2 mr-3', 'w-3 h-3', 'border border-primary-white', 'rounded-full shadow-xs')}
style={{ background: color }}
/>

<div className="flex flex-col justify-between flex-1">
<div className="flex flex-row justify-between">
<Name className="mb-1 text-sm font-medium leading-tight">
{(nameI18nKey && <T id={nameI18nKey} />) || name}
</Name>
<NetworkBadge networkName={networkName} />
</div>

<div
className={classNames('text-xs text-gray-700 font-light', 'flex items-center')}
style={{
marginBottom: '0.125rem'
}}
>
URL:<Name className="ml-1 font-normal">{rpcBaseURL}</Name>
</div>
</div>

{canRemove && (
<button
className={classNames(
'flex-none p-2',
'text-gray-500 hover:text-gray-600',
'transition ease-in-out duration-200'
)}
onClick={handleRemoveClick}
>
<CloseIcon className="w-auto h-5 stroke-current stroke-2" title={t('delete')} />
</button>
)}
</div>
);
};
2 changes: 2 additions & 0 deletions src/app/templates/Settings/NetworkSettings/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import NetworkSettings from './CustomNetworksSettings';
export default NetworkSettings;
15 changes: 13 additions & 2 deletions src/back/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import browser, { Runtime } from 'webextension-polyfill';
import { NostrExtensionMessageType, NostrExtensionRequest, NostrExtensionResponse } from 'lib/intercom/nostr/typings';
import { AppState, TempleRequest, XTMessageType, XTSettings, XTSharedStorageKey } from 'lib/messaging';
import { createQueue } from 'lib/queue';
import { fetchKnownNetworks } from 'lib/temple/networks';

import { MenuItems, setMenuItemEnabled } from './context-menus';
import * as SignumDApp from './dapp';
Expand All @@ -13,6 +14,7 @@ import {
accountsUpdated,
inited,
locked,
networksUpdated,
settingsUpdated,
store,
toFront,
Expand All @@ -23,6 +25,7 @@ import {
import { Vault } from './vault';

const NaiveAddressCheck = /^(S|TS)-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{5}$/;

function isSignumAddress(selection: string) {
return (
(selection.length === 22 || selection.length === 23) && // fast check
Expand All @@ -47,8 +50,13 @@ const AUTODECLINE_AFTER = 60_000;
const enqueueUnlock = createQueue();

export async function init() {
const vaultExist = await Vault.isExist();
inited(vaultExist);
const [vaultExist, networks] = await Promise.all([Vault.isExist(), fetchKnownNetworks()]);
await browser.storage.local.set({ networks });
networksUpdated(networks);
inited({
inited: vaultExist,
networks
});
}

export async function getFrontState(): Promise<AppState> {
Expand Down Expand Up @@ -120,6 +128,7 @@ export function revealPublicKey(accPublicKey: string) {
export function revealNostrPrivateKey(accPublicKey: string, password: string) {
return withUnlocked(() => Vault.revealNostrPrivateKey(accPublicKey, password));
}

export function removeAccount(accPublicKey: string, password: string) {
return withUnlocked(async () => {
const updatedAccounts = await Vault.removeAccount(accPublicKey, password);
Expand Down Expand Up @@ -152,6 +161,7 @@ export function importMnemonicAccount(mnemonic: string, name?: string, withNostr
accountsUpdated(updatedAccounts);
});
}

export function importAccountFromNostrPrivateKey(nsecOrHex: string, name?: string) {
return withUnlocked(async ({ vault }) => {
const updatedAccounts = await vault.importAccountFromNostrPrivKey(nsecOrHex, name);
Expand Down Expand Up @@ -298,6 +308,7 @@ async function createCustomNetworksSnapshot(settings: XTSettings) {
}
} catch {}
}

async function createNostrRelaysSnapshot(settings: XTSettings) {
try {
if (settings.nostrRelays) {
Expand Down
19 changes: 10 additions & 9 deletions src/back/dapp/dapp.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import browser from 'webextension-polyfill';

import { DAppSession, DAppSessions, Network } from 'lib/messaging';
import { NETWORKS } from 'lib/temple/networks';

const STORAGE_KEY = 'dapp_sessions';

Expand Down Expand Up @@ -49,20 +48,22 @@ export async function getCurrentAccountInfo() {
}

export async function getCurrentNetworkHost() {
const { network_id: networkId, custom_networks_snapshot: customNetworksSnapshot } = await browser.storage.local.get([
'network_id',
'custom_networks_snapshot'
]);
const {
network_id: networkId,
custom_networks_snapshot: customNetworksSnapshot,
networks
} = await browser.storage.local.get(['network_id', 'networks', 'custom_networks_snapshot']);

const allNetworks = [...NETWORKS, ...(customNetworksSnapshot ?? [])] as Network[];
const allNetworks = [...networks, ...(customNetworksSnapshot ?? [])] as Network[];
return allNetworks.find(n => !n.disabled && !n.hidden && n.id === networkId) as Network;
}

export async function getNetworkHosts(networkName: string) {
const { custom_networks_snapshot: customNetworksSnapshot } = await browser.storage.local.get(
const { custom_networks_snapshot: customNetworksSnapshot, networks } = await browser.storage.local.get([
'networks',
'custom_networks_snapshot'
);
]);

const allNetworks = [...NETWORKS, ...(customNetworksSnapshot ?? [])] as Network[];
const allNetworks = [...networks, ...(customNetworksSnapshot ?? [])] as Network[];
return allNetworks.filter(n => !n.disabled && !n.hidden && n.networkName === networkName);
}
1 change: 1 addition & 0 deletions src/back/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export async function start() {
initOmnibox();
await Actions.init();
const frontStore = store.map(toFront);

frontStore.watch(() => {
intercom.broadcast({ type: XTMessageType.StateUpdated });
});
Expand Down
15 changes: 12 additions & 3 deletions src/back/store.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { tr } from 'date-fns/locale';

Check warning on line 1 in src/back/store.test.ts

View workflow job for this annotation

GitHub Actions / Checks if ts, lint, unit tests & build works

'tr' is defined but never used
import browser from 'webextension-polyfill';

import { XTAccountType, WalletStatus } from 'lib/messaging';

import { accountsUpdated, inited as initEvent, locked, settingsUpdated, store, unlocked } from './store';
import {
accountsUpdated,
inited as initEvent,
locked,
networksUpdated,

Check warning on line 10 in src/back/store.test.ts

View workflow job for this annotation

GitHub Actions / Checks if ts, lint, unit tests & build works

'networksUpdated' is defined but never used
settingsUpdated,
store,
unlocked
} from './store';
import { Vault } from './vault';

describe('Store tests', () => {
Expand All @@ -22,13 +31,13 @@ describe('Store tests', () => {
expect(settings).toBeNull();
});
it('Inited event', () => {
initEvent(false);
initEvent({ inited: false, networks: [] });
const { inited, status } = store.getState();
expect(inited).toBeTruthy();
expect(status).toBe(WalletStatus.Idle);
});
it('Inited event with Vault', () => {
initEvent(true);
initEvent({ inited: true, networks: [] });
const { status } = store.getState();
expect(status).toBe(WalletStatus.Locked);
});
Expand Down
Loading

0 comments on commit ac6f9ae

Please sign in to comment.