check timeout when doing a query to /keys to add an exchange

This commit is contained in:
Sebastian 2021-12-06 15:27:20 -03:00
parent ce3ffbcd81
commit caa9a22d69
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
4 changed files with 130 additions and 64 deletions

View File

@ -43,14 +43,37 @@ export async function queryToSlashConfig<T>(
.then(getJsonIfOk); .then(getJsonIfOk);
} }
function timeout<T>(ms: number, promise: Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Timeout: the query took longer than ${Math.floor(ms / 1000)} secs`))
}, ms)
promise
.then(value => {
clearTimeout(timer)
resolve(value)
})
.catch(reason => {
clearTimeout(timer)
reject(reason)
})
})
}
export async function queryToSlashKeys<T>( export async function queryToSlashKeys<T>(
url: string, url: string,
): Promise<T> { ): Promise<T> {
return fetch(new URL("keys", url).href) const endpoint = new URL("keys", url)
endpoint.searchParams.set("cacheBreaker", new Date().getTime() + "");
const query = fetch(endpoint.href)
.catch(() => { .catch(() => {
throw new Error(`Network error`); throw new Error(`Network error`);
}) })
.then(getJsonIfOk); .then(getJsonIfOk);
return timeout(3000, query)
} }
export function buildTermsOfServiceState(tos: GetExchangeTosResult): TermsState { export function buildTermsOfServiceState(tos: GetExchangeTosResult): TermsState {

View File

@ -47,8 +47,15 @@ export function ExchangeAddPage({ onBack }: Props): VNode {
return ( return (
<ExchangeSetUrlPage <ExchangeSetUrlPage
onCancel={onBack} onCancel={onBack}
knownExchanges={knownExchanges} onVerify={async (url) => {
onVerify={(url) => queryToSlashKeys(url)} const found =
knownExchanges.findIndex((e) => e.exchangeBaseUrl === url) !== -1;
if (found) {
throw Error("This exchange is already known");
}
return queryToSlashKeys(url);
}}
onConfirm={(url) => onConfirm={(url) =>
queryToSlashKeys<TalerConfigResponse>(url) queryToSlashKeys<TalerConfigResponse>(url)
.then((config) => { .then((config) => {

View File

@ -36,22 +36,18 @@ export default {
export const ExpectedUSD = createExample(TestedComponent, { export const ExpectedUSD = createExample(TestedComponent, {
expectedCurrency: "USD", expectedCurrency: "USD",
onVerify: queryToSlashKeys, onVerify: queryToSlashKeys,
knownExchanges: [],
}); });
export const ExpectedKUDOS = createExample(TestedComponent, { export const ExpectedKUDOS = createExample(TestedComponent, {
expectedCurrency: "KUDOS", expectedCurrency: "KUDOS",
onVerify: queryToSlashKeys, onVerify: queryToSlashKeys,
knownExchanges: [],
}); });
export const InitialState = createExample(TestedComponent, { export const InitialState = createExample(TestedComponent, {
onVerify: queryToSlashKeys, onVerify: queryToSlashKeys,
knownExchanges: [],
}); });
export const WithDemoAsKnownExchange = createExample(TestedComponent, { const knownExchanges = [
knownExchanges: [
{ {
currency: "TESTKUDOS", currency: "TESTKUDOS",
exchangeBaseUrl: "https://exchange.demo.taler.net/", exchangeBaseUrl: "https://exchange.demo.taler.net/",
@ -63,6 +59,16 @@ export const WithDemoAsKnownExchange = createExample(TestedComponent, {
}, },
paytoUris: [], paytoUris: [],
}, },
], ];
onVerify: queryToSlashKeys,
export const WithDemoAsKnownExchange = createExample(TestedComponent, {
onVerify: async (url) => {
const found =
knownExchanges.findIndex((e) => e.exchangeBaseUrl === url) !== -1;
if (found) {
throw Error("This exchange is already known");
}
return queryToSlashKeys(url);
},
}); });

View File

@ -17,52 +17,75 @@ import {
export interface Props { export interface Props {
initialValue?: string; initialValue?: string;
expectedCurrency?: string; expectedCurrency?: string;
knownExchanges: ExchangeListItem[];
onCancel: () => void; onCancel: () => void;
onVerify: (s: string) => Promise<TalerConfigResponse | undefined>; onVerify: (s: string) => Promise<TalerConfigResponse | undefined>;
onConfirm: (url: string) => Promise<string | undefined>; onConfirm: (url: string) => Promise<string | undefined>;
withError?: string; withError?: string;
} }
function useEndpointStatus<T>(
endpoint: string,
onVerify: (e: string) => Promise<T>,
): {
loading: boolean;
error?: string;
endpoint: string;
result: T | undefined;
updateEndpoint: (s: string) => void;
} {
const [value, setValue] = useState<string>(endpoint);
const [dirty, setDirty] = useState(false);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<T | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
const [handler, setHandler] = useState<number | undefined>(undefined);
useEffect(() => {
if (!value) return;
window.clearTimeout(handler);
const h = window.setTimeout(async () => {
setDirty(true);
setLoading(true);
try {
const url = canonicalizeBaseUrl(value);
const result = await onVerify(url);
setResult(result);
setError(undefined);
setLoading(false);
} catch (e) {
const errorMessage =
e instanceof Error ? e.message : `unknown error: ${e}`;
setError(errorMessage);
setLoading(false);
setResult(undefined);
}
}, 500);
setHandler(h);
}, [value]);
return {
error: dirty ? error : undefined,
loading: loading,
result: result,
endpoint: value,
updateEndpoint: setValue,
};
}
export function ExchangeSetUrlPage({ export function ExchangeSetUrlPage({
initialValue, initialValue,
knownExchanges,
expectedCurrency, expectedCurrency,
onCancel, onCancel,
onVerify, onVerify,
onConfirm, onConfirm,
withError,
}: Props) { }: Props) {
const [value, setValue] = useState<string>(initialValue || ""); const { loading, result, endpoint, updateEndpoint, error } =
const [dirty, setDirty] = useState(false); useEndpointStatus(initialValue ?? "", onVerify);
const [result, setResult] = useState<TalerConfigResponse | undefined>(
undefined,
);
const [error, setError] = useState<string | undefined>(withError);
useEffect(() => { const [confirmationError, setConfirmationError] = useState<
try { string | undefined
const url = canonicalizeBaseUrl(value); >(undefined);
const found =
knownExchanges.findIndex((e) => e.exchangeBaseUrl === url) !== -1;
if (found) {
setError("This exchange is already known");
return;
}
onVerify(url)
.then((r) => {
setResult(r);
})
.catch(() => {
setResult(undefined);
});
setDirty(true);
} catch {
setResult(undefined);
}
}, [value]);
return ( return (
<Fragment> <Fragment>
@ -72,21 +95,32 @@ export function ExchangeSetUrlPage({
) : ( ) : (
<h2>Add exchange for {expectedCurrency}</h2> <h2>Add exchange for {expectedCurrency}</h2>
)} )}
{result && expectedCurrency && expectedCurrency !== result.currency && (
<WarningBox>
This exchange doesn't match the expected currency{" "}
<b>{expectedCurrency}</b>
</WarningBox>
)}
<ErrorMessage <ErrorMessage
title={error && "Unable to add this exchange"} title={error && "Unable to add this exchange"}
description={error} description={error}
/> />
<ErrorMessage
title={confirmationError && "Unable to add this exchange"}
description={confirmationError}
/>
<p> <p>
<Input invalid={dirty && !!error}> <Input invalid={!!error}>
<label>URL</label> <label>URL</label>
<input <input
type="text" type="text"
placeholder="https://" placeholder="https://"
value={value} value={endpoint}
onInput={(e) => setValue(e.currentTarget.value)} onInput={(e) => updateEndpoint(e.currentTarget.value)}
/> />
</Input> </Input>
{result && ( {loading && <div>loading... </div>}
{result && !loading && (
<Fragment> <Fragment>
<Input> <Input>
<label>Version</label> <label>Version</label>
@ -100,12 +134,6 @@ export function ExchangeSetUrlPage({
)} )}
</p> </p>
</section> </section>
{result && expectedCurrency && expectedCurrency !== result.currency && (
<WarningBox>
This exchange doesn't match the expected currency{" "}
<b>{expectedCurrency}</b>
</WarningBox>
)}
<footer> <footer>
<Button onClick={onCancel}> <Button onClick={onCancel}>
<i18n.Translate>Cancel</i18n.Translate> <i18n.Translate>Cancel</i18n.Translate>
@ -118,8 +146,10 @@ export function ExchangeSetUrlPage({
expectedCurrency !== result.currency) expectedCurrency !== result.currency)
} }
onClick={() => { onClick={() => {
const url = canonicalizeBaseUrl(value); const url = canonicalizeBaseUrl(endpoint);
return onConfirm(url).then((r) => (r ? setError(r) : undefined)); return onConfirm(url).then((r) =>
r ? setConfirmationError(r) : undefined,
);
}} }}
> >
<i18n.Translate>Next</i18n.Translate> <i18n.Translate>Next</i18n.Translate>