diff --git a/.changeset/late-pears-smash.md b/.changeset/late-pears-smash.md new file mode 100644 index 000000000..a4f10e2b1 --- /dev/null +++ b/.changeset/late-pears-smash.md @@ -0,0 +1,7 @@ +--- +'hostd': minor +'renterd': minor +'@siafoundation/design-system': minor +--- + +Fixed an issue where fiat input fields values were not displaying properly. diff --git a/apps/hostd/contexts/config/index.tsx b/apps/hostd/contexts/config/index.tsx index 98d477a91..3bd34eeb1 100644 --- a/apps/hostd/contexts/config/index.tsx +++ b/apps/hostd/contexts/config/index.tsx @@ -17,6 +17,7 @@ import { getFields } from './fields' import { calculateMaxCollateral, transformDown, transformUp } from './transform' import { useForm } from 'react-hook-form' import useLocalStorageState from 'use-local-storage-state' +import { useAppSettings } from '@siafoundation/react-core' export function useConfigMain() { const settings = useSettings({ @@ -95,8 +96,21 @@ export function useConfigMain() { return } try { + const calculatedValues: Partial = {} + if (!showAdvanced) { + calculatedValues.maxCollateral = calculateMaxCollateral( + values.storagePrice, + values.collateralMultiplier + ) + } + + const finalValues = { + ...values, + ...calculatedValues, + } + const response = await settingsUpdate.patch({ - payload: transformUp(values, settings.data), + payload: transformUp(finalValues, settings.data), }) if (response.error) { throw Error(response.error) @@ -117,7 +131,7 @@ export function useConfigMain() { console.log(e) } }, - [form, settings, settingsUpdate, revalidateAndResetFormData] + [form, showAdvanced, settings, settingsUpdate, revalidateAndResetFormData] ) const fields = useMemo(() => getFields({ showAdvanced }), [showAdvanced]) @@ -129,26 +143,6 @@ export function useConfigMain() { [form, onValid, onInvalid] ) - const storage = form.watch('storagePrice') - const collateralMultiplier = form.watch('collateralMultiplier') - - // if simple mode, then calculate and set max collateral - useEffect(() => { - if ( - !showAdvanced && - storage?.isGreaterThan(0) && - collateralMultiplier?.isGreaterThan(0) - ) { - form.setValue( - 'maxCollateral', - calculateMaxCollateral(storage, collateralMultiplier), - { - shouldDirty: true, - } - ) - } - }, [form, showAdvanced, storage, collateralMultiplier]) - // Resets so that stale values that are no longer in sync with what is on // the daemon will show up as changed. const resetWithUserChanges = useCallback(() => { @@ -166,6 +160,14 @@ export function useConfigMain() { } }, [form, resetFormDataIfAllDataFetched]) + const { isUnlocked } = useAppSettings() + useEffect(() => { + if (isUnlocked) { + revalidateAndResetFormData() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isUnlocked]) + useEffect(() => { if (form.formState.isSubmitting) { return diff --git a/apps/renterd/contexts/config/fields.tsx b/apps/renterd/contexts/config/fields.tsx index f3a6f41d4..069cb73ae 100644 --- a/apps/renterd/contexts/config/fields.tsx +++ b/apps/renterd/contexts/config/fields.tsx @@ -109,10 +109,12 @@ export function getFields({ units: 'SC/month', decimalsLimitSc: scDecimalPlaces, hidden: !isAutopilotEnabled || !showAdvanced, - // always required, but set in background unless advanced mode - validation: { - required: 'required', - }, + validation: + isAutopilotEnabled && showAdvanced + ? { + required: 'required', + } + : {}, }, periodWeeks: { type: 'number', diff --git a/apps/renterd/contexts/config/index.tsx b/apps/renterd/contexts/config/index.tsx index 0a202a666..7b60282f7 100644 --- a/apps/renterd/contexts/config/index.tsx +++ b/apps/renterd/contexts/config/index.tsx @@ -20,7 +20,7 @@ import { UploadPackingSettings, useSettingUpdate, } from '@siafoundation/react-renterd' -import { toScale, toSiacoins } from '@siafoundation/sia-js' +import { toSiacoins } from '@siafoundation/sia-js' import { getFields } from './fields' import { SettingsData, defaultValues } from './types' import { @@ -36,7 +36,7 @@ import { } from './transform' import { useForm } from 'react-hook-form' import { useSyncContractSet } from './useSyncContractSet' -import { delay, useMutate } from '@siafoundation/react-core' +import { delay, useAppSettings, useMutate } from '@siafoundation/react-core' import { useContractSetSettings } from '../../hooks/useContractSetSettings' import { ConfigDisplayOptions, @@ -262,6 +262,12 @@ export function useConfigMain() { resetFormData, ]) + const maxStoragePriceTBMonth = form.watch('maxStoragePriceTBMonth') + const maxDownloadPriceTB = form.watch('maxDownloadPriceTB') + const maxUploadPriceTB = form.watch('maxUploadPriceTB') + const storageTB = form.watch('storageTB') + const downloadTBMonth = form.watch('downloadTBMonth') + const uploadTBMonth = form.watch('uploadTBMonth') const minShards = form.watch('minShards') const totalShards = form.watch('totalShards') const includeRedundancyMaxStoragePrice = form.watch( @@ -270,14 +276,11 @@ export function useConfigMain() { const includeRedundancyMaxUploadPrice = form.watch( 'includeRedundancyMaxUploadPrice' ) - const storageTB = form.watch('storageTB') - const allowanceMonth = form.watch('allowanceMonth') - const maxStoragePriceTBMonth = form.watch('maxStoragePriceTBMonth') - const redundancyMultiplier = useMemo( () => getRedundancyMultiplier(minShards, totalShards), [minShards, totalShards] ) + const fields = useMemo(() => { if (averages.data) { return getFields({ @@ -328,6 +331,64 @@ export function useConfigMain() { includeRedundancyMaxUploadPrice, ]) + const canEstimate = useMemo(() => { + if (!isAutopilotEnabled) { + return false + } + return ( + maxStoragePriceTBMonth?.gt(0) && + storageTB?.gt(0) && + maxDownloadPriceTB?.gt(0) && + downloadTBMonth?.gt(0) && + maxUploadPriceTB?.gt(0) && + uploadTBMonth?.gt(0) + ) + }, [ + isAutopilotEnabled, + maxStoragePriceTBMonth, + storageTB, + maxDownloadPriceTB, + downloadTBMonth, + maxUploadPriceTB, + uploadTBMonth, + ]) + + const estimatedSpendingPerMonth = useMemo(() => { + if (!canEstimate) { + return new BigNumber(0) + } + const storageCostPerMonth = includeRedundancyMaxStoragePrice + ? maxStoragePriceTBMonth.times(storageTB) + : maxStoragePriceTBMonth.times(redundancyMultiplier).times(storageTB) + const downloadCostPerMonth = maxDownloadPriceTB.times(downloadTBMonth) + const uploadCostPerMonth = includeRedundancyMaxUploadPrice + ? maxUploadPriceTB.times(uploadTBMonth) + : maxUploadPriceTB.times(redundancyMultiplier).times(uploadTBMonth) + const totalCostPerMonth = storageCostPerMonth + .plus(downloadCostPerMonth) + .plus(uploadCostPerMonth) + return totalCostPerMonth + }, [ + canEstimate, + includeRedundancyMaxStoragePrice, + includeRedundancyMaxUploadPrice, + redundancyMultiplier, + maxStoragePriceTBMonth, + storageTB, + maxDownloadPriceTB, + downloadTBMonth, + maxUploadPriceTB, + uploadTBMonth, + ]) + + const estimatedSpendingPerTB = useMemo(() => { + if (!canEstimate) { + return new BigNumber(0) + } + const totalCostPerMonthTB = estimatedSpendingPerMonth.div(storageTB) + return totalCostPerMonthTB + }, [canEstimate, estimatedSpendingPerMonth, storageTB]) + const mutate = useMutate() const onValid = useCallback( async (values: typeof defaultValues) => { @@ -335,42 +396,62 @@ export function useConfigMain() { return } try { + const calculatedValues: Partial = {} + if (isAutopilotEnabled && !showAdvanced) { + calculatedValues.allowanceMonth = estimatedSpendingPerMonth + } + + const finalValues = { + ...values, + ...calculatedValues, + } + const firstTimeSettingConfig = isAutopilotEnabled && !autopilot.data const autopilotResponse = isAutopilotEnabled ? await autopilotUpdate.put({ - payload: transformUpAutopilot(values, autopilot.data), + payload: transformUpAutopilot(finalValues, autopilot.data), }) : undefined - const contractSetResponse = await settingUpdate.put({ - params: { - key: 'contractset', - }, - payload: transformUpContractSet(values, contractSet.data), - }) - const uploadPackingResponse = await settingUpdate.put({ - params: { - key: 'uploadpacking', - }, - payload: transformUpUploadPacking(values, uploadPacking.data), - }) - const gougingResponse = await settingUpdate.put({ - params: { - key: 'gouging', - }, - payload: transformUpGouging(values, gouging.data), - }) - const redundancyResponse = await settingUpdate.put({ - params: { - key: 'redundancy', - }, - payload: transformUpRedundancy(values, redundancy.data), - }) - const configAppResponse = await settingUpdate.put({ - params: { - key: configDisplayOptionsKey, - }, - payload: transformUpConfigApp(values, configApp.data), - }) + + const [ + contractSetResponse, + uploadPackingResponse, + gougingResponse, + redundancyResponse, + configAppResponse, + ] = await Promise.all([ + settingUpdate.put({ + params: { + key: 'contractset', + }, + payload: transformUpContractSet(finalValues, contractSet.data), + }), + settingUpdate.put({ + params: { + key: 'uploadpacking', + }, + payload: transformUpUploadPacking(finalValues, uploadPacking.data), + }), + settingUpdate.put({ + params: { + key: 'gouging', + }, + payload: transformUpGouging(finalValues, gouging.data), + }), + settingUpdate.put({ + params: { + key: 'redundancy', + }, + payload: transformUpRedundancy(finalValues, redundancy.data), + }), + settingUpdate.put({ + params: { + key: configDisplayOptionsKey, + }, + payload: transformUpConfigApp(finalValues, configApp.data), + }), + ]) + if (autopilotResponse?.error) { throw Error(autopilotResponse.error) } @@ -392,7 +473,7 @@ export function useConfigMain() { triggerSuccessToast('Configuration has been saved.') if (isAutopilotEnabled) { - syncDefaultContractSet(values.autopilotContractSet) + syncDefaultContractSet(finalValues.autopilotContractSet) } // if autopilot is being configured for the first time, @@ -414,6 +495,8 @@ export function useConfigMain() { } }, [ + estimatedSpendingPerMonth, + showAdvanced, isAutopilotEnabled, autopilot, autopilotUpdate, @@ -436,64 +519,6 @@ export function useConfigMain() { [form, onValid, onInvalid] ) - const canEstimate = useMemo(() => { - if (!isAutopilotEnabled) { - return false - } - return !( - !storageTB || - !allowanceMonth || - storageTB.isZero() || - allowanceMonth.isZero() - ) - }, [isAutopilotEnabled, storageTB, allowanceMonth]) - - const estimatedSpendingPerMonth = useMemo(() => { - if (!canEstimate) { - return new BigNumber(0) - } - return toScale(allowanceMonth, 0) - }, [canEstimate, allowanceMonth]) - - const estimatedSpendingPerTB = useMemo(() => { - if (!canEstimate) { - return new BigNumber(0) - } - const estimatedSpendingPerMonthTB = estimatedSpendingPerMonth.div(storageTB) - return toScale(estimatedSpendingPerMonthTB, 0) - }, [canEstimate, estimatedSpendingPerMonth, storageTB]) - - // if simple mode, then calculate and set allowance when dependent values change - useEffect(() => { - if ( - !showAdvanced && - storageTB?.isGreaterThan(0) && - maxStoragePriceTBMonth?.isGreaterThan(0) - ) { - form.setValue( - 'allowanceMonth', - maxStoragePriceTBMonth - .times(storageTB) - .times( - !includeRedundancyMaxStoragePrice - ? getRedundancyMultiplier(minShards, totalShards) - : 1 - ), - { - shouldDirty: true, - } - ) - } - }, [ - form, - showAdvanced, - storageTB, - maxStoragePriceTBMonth, - minShards, - totalShards, - includeRedundancyMaxStoragePrice, - ]) - // Resets so that stale values that are no longer in sync with what is on // the daemon will show up as changed. const resetWithUserChanges = useCallback(() => { @@ -511,6 +536,14 @@ export function useConfigMain() { } }, [form, resetFormDataIfAllDataFetched]) + const { isUnlocked } = useAppSettings() + useEffect(() => { + if (isUnlocked && app.autopilot.status !== 'init') { + revalidateAndResetFormData() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isUnlocked, app.autopilot.status]) + useEffect(() => { if (form.formState.isSubmitting) { return diff --git a/libs/design-system/src/core/SiacoinField.spec.tsx b/libs/design-system/src/core/SiacoinField.spec.tsx index 8f54daa8a..269867f7e 100644 --- a/libs/design-system/src/core/SiacoinField.spec.tsx +++ b/libs/design-system/src/core/SiacoinField.spec.tsx @@ -1,4 +1,8 @@ -import { AppSettingsProvider, CoreProvider } from '@siafoundation/react-core' +import { + AppSettingsProvider, + CoreProvider, + delay, +} from '@siafoundation/react-core' import BigNumber from 'bignumber.js' import { SiacoinField } from './SiacoinField' import { fireEvent, render, waitFor } from '@testing-library/react' @@ -333,6 +337,8 @@ async function renderNode({ const scInput = node.getByTestId('scInput') as HTMLInputElement const fiatInput = node.getByTestId('fiatInput') as HTMLInputElement await waitFor(() => expect(fiatInput.value).toBeTruthy()) + // let the component set state and finish the next render pass + await delay(0) return { node, scInput, fiatInput } } diff --git a/libs/design-system/src/core/SiacoinField.tsx b/libs/design-system/src/core/SiacoinField.tsx index 618c8e12b..afe7837b2 100644 --- a/libs/design-system/src/core/SiacoinField.tsx +++ b/libs/design-system/src/core/SiacoinField.tsx @@ -127,23 +127,30 @@ export const SiacoinField = forwardRef(function SiacoinField( [updateSc, rate] ) + const [hasInitializedSc, setHasInitializedSc] = useState(false) // sync externally controlled value useEffect(() => { if (!externalSc.isEqualTo(sc)) { const fesc = toFixedMax(externalSc, decimalsLimitSc) setLocalSc(fesc) // sync fiat if its not active, syncing it when it is being changed - // may change the decmials as the user is typing. + // may change the decimals as the user is typing. if (active !== 'fiat') { syncFiatToSc(fesc) } } + if (!hasInitializedSc) { + setHasInitializedSc(true) + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [externalSc]) - // initialize fiat once rate has loaded + // initialize fiat once rate has loaded, + // but only if the siacoin value has initialized useEffect(() => { - syncFiatToSc(sc) + if (hasInitializedSc) { + syncFiatToSc(sc) + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [rate])