Skip to content

Commit f10d592

Browse files
authored
Improve cost estimation (#441)
* update cost estimation error design * direct command fallback for cost estimation
1 parent e3a6e2d commit f10d592

3 files changed

Lines changed: 142 additions & 66 deletions

File tree

src/components/run-job/select-resources.module.css

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,6 @@
6262
}
6363

6464
.costCard {
65-
display: flex;
66-
flex-direction: column;
67-
gap: 4px;
68-
padding: 12px 24px;
69-
7065
.costEstimation {
7166
align-items: center;
7267
display: flex;

src/components/run-job/select-resources.tsx

Lines changed: 80 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import useEnvResources from '@/components/hooks/use-env-resources';
55
import Input from '@/components/input/input';
66
import Select from '@/components/input/select';
77
import Slider from '@/components/slider/slider';
8+
import config from '@/config';
89
import { SelectedToken, useRunJobContext } from '@/context/run-job-context';
910
import { useP2P } from '@/contexts/P2PContext';
1011
import { useOceanAccount } from '@/lib/use-ocean-account';
@@ -41,6 +42,8 @@ const SelectResources = ({ environment, freeCompute, token }: SelectResourcesPro
4142
const { closeAuthModal, isOpen: isAuthModalOpen, openAuthModal } = useAuthModal();
4243
const router = useRouter();
4344

45+
const { isReady: p2pReady } = useP2P();
46+
4447
const { account } = useOceanAccount();
4548

4649
const {
@@ -52,8 +55,6 @@ const SelectResources = ({ environment, freeCompute, token }: SelectResourcesPro
5255
setSelectedResources,
5356
} = useRunJobContext();
5457

55-
const { isReady: p2pReady } = useP2P();
56-
5758
const [initComputeError, setInitComputeError] = useState<unknown | null>(null);
5859
const [isLoadingCost, setIsLoadingCost] = useState(false);
5960

@@ -275,45 +276,83 @@ const SelectResources = ({ environment, freeCompute, token }: SelectResourcesPro
275276
formik.setFieldValue('maxJobDurationValue', Math.max(0, num));
276277
};
277278

278-
const renderCostEstimation = () => {
279-
if (isLoadingCost) {
280-
return (
281-
<h3 className={styles.estimationMessage}>
282-
<CircularProgress size={24} />
283-
Estimating cost...
284-
</h3>
285-
);
286-
}
287-
if (!!initComputeError || !token) {
288-
let errorText;
289-
if (initComputeError instanceof Error) {
290-
errorText = initComputeError.message;
279+
const renderCostCard = () => {
280+
const renderCostEstimation = () => {
281+
if (isLoadingCost) {
282+
return (
283+
<h3 className={styles.estimationMessage}>
284+
<CircularProgress size={24} />
285+
Estimating cost...
286+
</h3>
287+
);
288+
}
289+
if (!p2pReady || (!estimatedTotalCost && estimatedTotalCost !== 0)) {
290+
return (
291+
<h3 className={styles.estimationMessage}>
292+
<CircularProgress size={24} />
293+
Connecting to node...
294+
</h3>
295+
);
291296
}
292297
return (
293-
<h3 className={styles.estimationMessage}>
294-
Cost estimation failed{' '}
295-
{errorText ? (
296-
<Tooltip className="textAccent1" title={errorText}>
297-
<InfoOutlinedIcon className={styles.accessInfoIcon} />
298-
</Tooltip>
299-
) : null}
300-
</h3>
298+
<div>
299+
<span className={styles.token}>{token?.symbol}</span>
300+
&nbsp;
301+
<span className={styles.amount}>{token ? formatTokenAmount(estimatedTotalCost, token.address) : null}</span>
302+
</div>
301303
);
304+
};
305+
306+
return (
307+
<Card
308+
className={styles.costCard}
309+
direction="column"
310+
innerShadow="black"
311+
paddingX="md"
312+
paddingY="sm"
313+
radius="md"
314+
spacing="sm"
315+
variant="glass"
316+
>
317+
<div className={styles.costEstimation}>
318+
<h3>Estimated total cost</h3>
319+
{renderCostEstimation()}
320+
</div>
321+
<div className="alignSelfEnd textSuccessDarker">
322+
If your job finishes earlier than estimated, the unconsumed tokens remain in your escrow
323+
</div>
324+
</Card>
325+
);
326+
};
327+
328+
const renderConnectionErrorCard = () => {
329+
if (!initComputeError) {
330+
return null;
302331
}
303-
if (!p2pReady || (!estimatedTotalCost && estimatedTotalCost !== 0)) {
304-
return (
305-
<h3 className={styles.estimationMessage}>
306-
<CircularProgress size={24} />
307-
Connecting to node...
308-
</h3>
309-
);
332+
let errorText;
333+
if (initComputeError instanceof Error) {
334+
errorText = initComputeError.message;
310335
}
311336
return (
312-
<div>
313-
<span className={styles.token}>{token?.symbol}</span>
314-
&nbsp;
315-
<span className={styles.amount}>{formatTokenAmount(estimatedTotalCost, token.address)}</span>
316-
</div>
337+
<Card direction="column" paddingX="md" paddingY="sm" radius="md" spacing="sm" variant="error">
338+
<h3>Could not reach this node</h3>
339+
{errorText ? <p>{errorText}</p> : null}
340+
<p>
341+
This may be due to missing WSS, TLS, or P2P circuit relay configuration on the node.
342+
<br />
343+
If you are the node operator, please check your node&apos;s network setup.
344+
</p>
345+
<Button
346+
className="alignSelfStart"
347+
color="accent2"
348+
href={config.links.docs}
349+
size="sm"
350+
target="_blank"
351+
variant="filled"
352+
>
353+
Visit docs
354+
</Button>
355+
</Card>
317356
);
318357
};
319358

@@ -439,21 +478,12 @@ const SelectResources = ({ environment, freeCompute, token }: SelectResourcesPro
439478
value={formik.values.maxJobDurationValue}
440479
/>
441480
</div>
442-
<TransitionGroup>
443-
{formik.isValid && !freeCompute ? (
444-
<Collapse>
445-
<Card className={styles.costCard} innerShadow="black" radius="md" variant="glass">
446-
<div className={styles.costEstimation}>
447-
<h3>Estimated total cost</h3>
448-
{renderCostEstimation()}
449-
</div>
450-
<div className="alignSelfEnd textSuccessDarker">
451-
If your job finishes earlier than estimated, the unconsumed tokens remain in your escrow
452-
</div>
453-
</Card>
454-
</Collapse>
455-
) : null}
456-
</TransitionGroup>
481+
{freeCompute ? null : (
482+
<TransitionGroup>
483+
{initComputeError ? <Collapse>{renderConnectionErrorCard()}</Collapse> : null}
484+
{!initComputeError && formik.isValid ? <Collapse>{renderCostCard()}</Collapse> : null}
485+
</TransitionGroup>
486+
)}
457487
<div className="actionsGroupLgBetween">
458488
<Button
459489
color="accent1"

src/context/run-job-context.tsx

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { getApiRoute } from '@/config';
22
import { CHAIN_ID } from '@/constants/chains';
33
import { useP2P } from '@/contexts/P2PContext';
4-
import { getTokenSymbol } from '@/lib/token-symbol';
4+
import { directNodeCommand } from '@/lib/direct-node-command';
5+
import { getTokenDecimals, getTokenSymbol } from '@/lib/token-symbol';
56
import { useOceanAccount } from '@/lib/use-ocean-account';
67
import {
78
ComputeEnvironment,
@@ -12,6 +13,7 @@ import {
1213
} from '@/types/environments';
1314
import { roundTokenAmount } from '@/utils/formatters';
1415
import axios from 'axios';
16+
import BigNumber from 'bignumber.js';
1517
import { useSearchParams } from 'next/navigation';
1618
import { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react';
1719

@@ -183,23 +185,72 @@ export const RunJobProvider = ({ children }: { children: ReactNode }) => {
183185
if (!provider || !p2pIsReady) {
184186
return;
185187
}
188+
const validUntil = maxJobDurationSeconds < 1 ? 1 : Math.ceil(maxJobDurationSeconds);
189+
let cost: string;
190+
let minLockSeconds: number;
191+
186192
try {
187-
const { cost, minLockSeconds } = await initializeCompute(
193+
const result = await initializeCompute(
188194
environment,
189195
tokenAddress,
190-
maxJobDurationSeconds < 1 ? 1 : Math.ceil(maxJobDurationSeconds),
196+
validUntil,
191197
multiaddrsOrPeerId,
192198
environment.consumerAddress,
193199
resources,
194200
CHAIN_ID
195201
);
196-
setEstimatedTotalCost(roundTokenAmount(Number(cost), tokenAddress, 'up'));
197-
setMinLockSeconds(minLockSeconds);
198-
onSuccess?.(Number(cost), minLockSeconds);
199-
} catch (error) {
200-
onError?.(error);
201-
console.error('Failed to fetch estimated cost:', error);
202+
cost = result.cost;
203+
minLockSeconds = result.minLockSeconds;
204+
} catch (p2pError) {
205+
console.warn('P2P cost estimation failed, falling back to direct node command:', p2pError);
206+
const payload = {
207+
datasets: [],
208+
algorithm: { meta: { rawcode: 'rawcode' } },
209+
environment: environment.id,
210+
payment: {
211+
chainId: CHAIN_ID,
212+
token: tokenAddress,
213+
resources,
214+
},
215+
maxJobDuration: validUntil,
216+
consumerAddress: environment.consumerAddress,
217+
signature: '',
218+
};
219+
try {
220+
const multiaddrs = Array.isArray(multiaddrsOrPeerId) ? multiaddrsOrPeerId : undefined;
221+
const peerId =
222+
typeof multiaddrsOrPeerId === 'string'
223+
? multiaddrsOrPeerId
224+
: (multiaddrs?.map((a) => a.match(/\/p2p\/(\S+)/)?.[1]).find(Boolean) ?? '');
225+
const response = await directNodeCommand({
226+
command: 'initializeCompute',
227+
body: payload,
228+
multiaddrs,
229+
peerId,
230+
});
231+
const data: {
232+
payment: { amount: string; minLockSeconds: number };
233+
status?: { httpStatus: number; error?: string };
234+
} = await response.json();
235+
if (data?.status?.httpStatus != null && data.status.httpStatus >= 400) {
236+
throw new Error(data.status.error ?? 'Initialize compute failed');
237+
}
238+
const tokenDecimals = await getTokenDecimals(tokenAddress);
239+
const decimalsNumber = Number(tokenDecimals);
240+
cost = new BigNumber(data.payment.amount)
241+
.div(new BigNumber(10).pow(decimalsNumber))
242+
.decimalPlaces(decimalsNumber)
243+
.toString();
244+
minLockSeconds = data.payment.minLockSeconds;
245+
} catch (directError) {
246+
onError?.(directError);
247+
console.error('Failed to fetch estimated cost:', directError);
248+
return;
249+
}
202250
}
251+
setEstimatedTotalCost(roundTokenAmount(Number(cost), tokenAddress, 'up'));
252+
setMinLockSeconds(minLockSeconds);
253+
onSuccess?.(Number(cost), minLockSeconds);
203254
},
204255
[initializeCompute, p2pIsReady, provider]
205256
);
@@ -324,7 +375,7 @@ export const RunJobProvider = ({ children }: { children: ReactNode }) => {
324375

325376
/**
326377
* Initiate hydration when initializing the context
327-
* If free compute was selected, we don't need to wait for the p2p node to be ready, but otherwise we need it to calculate cost
378+
* If free compute was selected, we don't need to wait for the p2p node/ provider, but otherwise we need it to calculate cost
328379
*/
329380
useEffect(() => {
330381
if (!hydrateFromUrlStarted) {
@@ -333,7 +384,7 @@ export const RunJobProvider = ({ children }: { children: ReactNode }) => {
333384
setHydrateFromUrlFinished(true);
334385
} else {
335386
const queryFree = searchParams.get('free') === 'true';
336-
// For paid compute, also wait for p2p node and provider in order to fetch the cost
387+
// For paid compute, wait for p2p node and provider in order to fetch the cost (direct command is the fallback)
337388
// For free compute, they are not needed
338389
if (queryFree || (p2pIsReady && provider)) {
339390
setHydrateFromUrlStarted(true);

0 commit comments

Comments
 (0)