diff --git a/.changeset/breezy-mayflies-kneel.md b/.changeset/breezy-mayflies-kneel.md
new file mode 100644
index 000000000..b06b07a10
--- /dev/null
+++ b/.changeset/breezy-mayflies-kneel.md
@@ -0,0 +1,5 @@
+---
+'renterd': minor
+---
+
+Configuration values such as download and upload utilization are now per month instead of period.
diff --git a/.changeset/breezy-spoons-rest.md b/.changeset/breezy-spoons-rest.md
new file mode 100644
index 000000000..dba276188
--- /dev/null
+++ b/.changeset/breezy-spoons-rest.md
@@ -0,0 +1,5 @@
+---
+'renterd': minor
+---
+
+The configuration now has an advanced mode that allows the user to view and change all settings.
diff --git a/.changeset/cool-yaks-accept.md b/.changeset/cool-yaks-accept.md
new file mode 100644
index 000000000..9badb648d
--- /dev/null
+++ b/.changeset/cool-yaks-accept.md
@@ -0,0 +1,5 @@
+---
+'renterd': minor
+---
+
+The configuration is now much simpler by default, only requiring storage amount and price to be set during onboarding.
diff --git a/.changeset/gorgeous-birds-push.md b/.changeset/gorgeous-birds-push.md
new file mode 100644
index 000000000..3846c1967
--- /dev/null
+++ b/.changeset/gorgeous-birds-push.md
@@ -0,0 +1,5 @@
+---
+'renterd': minor
+---
+
+The configuration page now shows the changed status on fields if the user has made a change but the server values were since updated.
diff --git a/.changeset/hip-cheetahs-fix.md b/.changeset/hip-cheetahs-fix.md
new file mode 100644
index 000000000..1c41f8b54
--- /dev/null
+++ b/.changeset/hip-cheetahs-fix.md
@@ -0,0 +1,5 @@
+---
+'renterd': minor
+---
+
+The default simple configuration automatically sets any unset advanced options to typical defaults. Settings with existing values are not changed.
diff --git a/.changeset/neat-shoes-cover.md b/.changeset/neat-shoes-cover.md
new file mode 100644
index 000000000..04936d32d
--- /dev/null
+++ b/.changeset/neat-shoes-cover.md
@@ -0,0 +1,5 @@
+---
+'renterd': minor
+---
+
+The autopilot settings have been merged into a single configuration page.
diff --git a/.changeset/rich-numbers-smoke.md b/.changeset/rich-numbers-smoke.md
new file mode 100644
index 000000000..1772be0b5
--- /dev/null
+++ b/.changeset/rich-numbers-smoke.md
@@ -0,0 +1,5 @@
+---
+'@siafoundation/react-core': minor
+---
+
+Request hooks now have a standalone option that allows them to revalidate under a unique key.
diff --git a/.changeset/weak-fans-search.md b/.changeset/weak-fans-search.md
new file mode 100644
index 000000000..7a462b3ae
--- /dev/null
+++ b/.changeset/weak-fans-search.md
@@ -0,0 +1,5 @@
+---
+'renterd': minor
+---
+
+The spending estimates now show per TB and total per month.
diff --git a/.changeset/wet-rings-hope.md b/.changeset/wet-rings-hope.md
new file mode 100644
index 000000000..74f7e13b6
--- /dev/null
+++ b/.changeset/wet-rings-hope.md
@@ -0,0 +1,5 @@
+---
+'renterd': minor
+---
+
+New users will now see an onboarding wizard that prompts the user to complete the necessary setup steps - it also shows the status and progress of each.
diff --git a/apps/renterd/components/Autopilot/fields.tsx b/apps/renterd/components/Autopilot/fields.tsx
deleted file mode 100644
index 2ce7d34cd..000000000
--- a/apps/renterd/components/Autopilot/fields.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-/* eslint-disable react/no-unescaped-entities */
-import { Code, ConfigFields } from '@siafoundation/design-system'
-import BigNumber from 'bignumber.js'
-import React from 'react'
-
-export const scDecimalPlaces = 6
-
-export const defaultValues = {
- // contracts
- set: '',
- amount: undefined as BigNumber | undefined,
- allowance: undefined as BigNumber | undefined,
- period: undefined as BigNumber | undefined,
- renewWindow: undefined as BigNumber | undefined,
- download: undefined as BigNumber | undefined,
- upload: undefined as BigNumber | undefined,
- storage: undefined as BigNumber | undefined,
- // hosts
- allowRedundantIPs: false,
- maxDowntimeHours: undefined as BigNumber | undefined,
- // wallet
- defragThreshold: undefined as BigNumber | undefined,
-}
-
-export type SettingsData = typeof defaultValues
-
-export const fields: ConfigFields<
- typeof defaultValues,
- 'contracts' | 'hosts' | 'wallet'
-> = {
- // contracts
- storage: {
- type: 'number',
- category: 'contracts',
- title: 'Expected storage',
- description: <>The amount of storage you would like to rent in TB.>,
- units: 'TB',
- validation: {
- required: 'required',
- },
- },
- upload: {
- type: 'number',
- category: 'contracts',
- title: 'Expected upload',
- description: (
- <>The amount of upload bandwidth you plan to use each period in TB.>
- ),
- units: 'TB/period',
- validation: {
- required: 'required',
- },
- },
- download: {
- type: 'number',
- category: 'contracts',
- title: 'Expected download',
- description: (
- <>The amount of download bandwidth you plan to use each period in TB.>
- ),
- units: 'TB/period',
- validation: {
- required: 'required',
- },
- },
- allowance: {
- type: 'siacoin',
- category: 'contracts',
- title: 'Allowance',
- description: (
- <>The amount of Siacoin you would like to spend for the period.>
- ),
- decimalsLimitSc: scDecimalPlaces,
- validation: {
- required: 'required',
- },
- },
- period: {
- type: 'number',
- category: 'contracts',
- title: 'Period',
- description: <>The length of the storage contracts.>,
- units: 'weeks',
- suggestion: new BigNumber(6),
- suggestionTip: 'Typically 6 weeks.',
- validation: {
- required: 'required',
- },
- },
- renewWindow: {
- type: 'number',
- category: 'contracts',
- title: 'Renew window',
- description: (
- <>
- The number of weeks prior to contract expiration that Sia will attempt
- to renew your contracts.
- >
- ),
- units: 'weeks',
- decimalsLimit: 6,
- suggestion: new BigNumber(2),
- suggestionTip: 'Typically 2 weeks.',
- validation: {
- required: 'required',
- },
- },
- amount: {
- type: 'number',
- category: 'contracts',
- title: 'Hosts',
- description: <>The number of hosts to create contracts with.>,
- units: 'hosts',
- decimalsLimit: 0,
- suggestion: new BigNumber(50),
- suggestionTip: 'Typically 50 hosts.',
- validation: {
- required: 'required',
- },
- },
- set: {
- type: 'text',
- category: 'contracts',
- title: 'Contract set',
- description: (
- <>
- The contract set that autopilot should use. This should typically be the
- same as the default contract set.
- >
- ),
- placeholder: 'autopilot',
- suggestion: 'autopilot',
- suggestionTip: (
- <>
- The default contract set is autopilot
.
- >
- ),
- validation: {
- required: 'required',
- },
- },
-
- // hosts
- allowRedundantIPs: {
- type: 'boolean',
- category: 'hosts',
- title: 'Redundant IPs',
- description: (
- <>
- Whether or not to allow forming contracts with multiple hosts in the
- same IP subnet. The subnets used are /16 for IPv4, and /64 for IPv6.
- >
- ),
- suggestion: false,
- suggestionTip: 'Defaults to off.',
- validation: {},
- },
- maxDowntimeHours: {
- type: 'number',
- category: 'hosts',
- title: 'Max downtime',
- description: (
- <>
- The maximum amount of host downtime that autopilot will tolerate in
- hours.
- >
- ),
- units: 'hours',
- suggestion: new BigNumber(1440),
- suggestionTip: 'Defaults to 1,440 which is 60 days.',
- validation: {
- required: 'required',
- },
- },
-
- // wallet
- defragThreshold: {
- type: 'number',
- category: 'wallet',
- title: 'Defrag threshold',
- description: <>The threshold after which autopilot will defrag outputs.>,
- units: 'outputs',
- suggestion: new BigNumber(1000),
- suggestionTip: 'Defaults to 1,000.',
- validation: {
- required: 'required',
- },
- },
-}
diff --git a/apps/renterd/components/Autopilot/index.tsx b/apps/renterd/components/Autopilot/index.tsx
deleted file mode 100644
index 2de702db6..000000000
--- a/apps/renterd/components/Autopilot/index.tsx
+++ /dev/null
@@ -1,288 +0,0 @@
-import {
- Text,
- Button,
- triggerSuccessToast,
- TBToBytes,
- ValueSc,
- useSiacoinFiat,
- ValueNum,
- ConfigurationPanel,
- triggerErrorToast,
- useOnInvalid,
- Switch,
- ControlGroup,
- Popover,
- Label,
- Paragraph,
-} from '@siafoundation/design-system'
-import { Reset16, Save16, Settings16 } from '@siafoundation/react-icons'
-import BigNumber from 'bignumber.js'
-import { useCallback, useEffect, useMemo, useState } from 'react'
-import { RenterdSidenav } from '../RenterdSidenav'
-import { routes } from '../../config/routes'
-import { useDialog } from '../../contexts/dialog'
-import { RenterdAuthedLayout } from '../RenterdAuthedLayout'
-import {
- AutopilotConfig,
- autopilotHostsKey,
- useAutopilotConfig,
- useAutopilotConfigUpdate,
-} from '@siafoundation/react-renterd'
-import { humanBytes, toHastings, toScale } from '@siafoundation/sia-js'
-import { defaultValues, fields } from './fields'
-import { transformDown, transformUp } from './transform'
-import { useForm } from 'react-hook-form'
-import { useSyncContractSet } from './useSyncContractSet'
-import { delay, useMutate } from '@siafoundation/react-core'
-
-export function Autopilot() {
- const { openDialog } = useDialog()
- const configUpdate = useAutopilotConfigUpdate()
- const config = useAutopilotConfig({
- // Do not automatically refetch
- config: {
- swr: {
- revalidateOnFocus: false,
- errorRetryCount: 0,
- },
- },
- })
- const {
- shouldSyncDefaultContractSet,
- setShouldSyncDefaultContractSet,
- syncDefaultContractSet,
- } = useSyncContractSet()
-
- const form = useForm({
- mode: 'all',
- defaultValues,
- })
-
- const resetFormData = useCallback(
- (data: AutopilotConfig) => {
- form.reset(transformDown(data))
- },
- [form]
- )
-
- // init - when new config is fetched, set the form
- const [hasInit, setHasInit] = useState(false)
- useEffect(() => {
- if (config.data && !hasInit) {
- resetFormData(config.data)
- setHasInit(true)
- return
- }
- if (config.error?.status === 404 && !hasInit) {
- form.reset(defaultValues)
- setHasInit(true)
- return
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [config.data, config.error])
-
- const reset = useCallback(async () => {
- const data = await config.mutate()
- if (!data) {
- triggerErrorToast('Error fetching config.')
- } else {
- resetFormData(data)
- }
- }, [config, resetFormData])
-
- const mutate = useMutate()
- const onValid = useCallback(
- async (values: typeof defaultValues) => {
- try {
- const firstTimeSettingConfig = !config.data
- await configUpdate.put({
- payload: transformUp(values, config.data),
- })
- triggerSuccessToast('Configuration has been saved.')
- syncDefaultContractSet(values.set)
-
- // if autopilot is being configured for the first time,
- // revalidate the empty hosts list.
- if (firstTimeSettingConfig) {
- const refreshHostsAfterDelay = async () => {
- await delay(5_000)
- mutate((key) => key.startsWith(autopilotHostsKey))
- await delay(5_000)
- mutate((key) => key.startsWith(autopilotHostsKey))
- }
- refreshHostsAfterDelay()
- }
-
- reset()
- } catch (e) {
- triggerErrorToast((e as Error).message)
- console.log(e)
- }
- },
- [config.data, configUpdate, reset, syncDefaultContractSet, mutate]
- )
-
- const onInvalid = useOnInvalid(fields)
-
- const onSubmit = useMemo(
- () => form.handleSubmit(onValid, onInvalid),
- [form, onValid, onInvalid]
- )
-
- const storage = form.watch('storage')
- const period = form.watch('period')
- const allowance = form.watch('allowance')
-
- const canEstimate = useMemo(() => {
- return !(
- !storage ||
- !period ||
- !allowance ||
- storage.isZero() ||
- period.isZero() ||
- allowance.isZero()
- )
- }, [storage, period, allowance])
-
- const estimatedSpending = useMemo(() => {
- if (!canEstimate) {
- return new BigNumber(0)
- }
- const estimatePerPeriod = allowance
- const estimatePerWeek = estimatePerPeriod.div(period)
- const estimatePerMonth = estimatePerWeek.div(7).times(30)
- return toScale(estimatePerMonth, 0)
- }, [canEstimate, period, allowance])
-
- const changeCount = Object.entries(form.formState.dirtyFields).filter(
- ([_, val]) => !!val
- ).length
-
- const { fiat, currency } = useSiacoinFiat({ sc: estimatedSpending })
-
- return (
- }
- stats={
- !canEstimate ? (
-
- Enter expected storage, period, and allowance values to estimate
- monthly spending.
-
- ) : (
-
-
- Estimate:
-
-
-
- {fiat && (
-
-
- `(${currency.prefix}${v.toFixed(currency.fixed)})`
- }
- />
-
- )}
-
- per month to store {humanBytes(TBToBytes(storage).toNumber())}
-
-
-
- )
- }
- actions={
-
- {!!changeCount && (
-
- {changeCount === 1 ? '1 change' : `${changeCount} changes`}
-
- )}
-
-
-
-
-
-
- Save changes
-
-
-
-
- }
- >
-
-
Options
-
-
- setShouldSyncDefaultContractSet(val)
- }
- >
- sync default contract set
-
-
- Automatically update the default contract set to be the same
- as the autopilot contract set when changes are saved.
-
-
-
-
-
-
- }
- openSettings={() => openDialog('settings')}
- >
-
-
-
-
-
-
- )
-}
diff --git a/apps/renterd/components/Autopilot/transform.spec.ts b/apps/renterd/components/Autopilot/transform.spec.ts
deleted file mode 100644
index 9e6267f64..000000000
--- a/apps/renterd/components/Autopilot/transform.spec.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import BigNumber from 'bignumber.js'
-import { transformDown, transformUp } from './transform'
-
-describe('data transforms', () => {
- it('down', () => {
- expect(
- transformDown({
- wallet: {
- defragThreshold: 1000,
- },
- hosts: {
- allowRedundantIPs: false,
- maxDowntimeHours: 1440,
- scoreOverrides: null,
- },
- contracts: {
- set: 'autopilot',
- amount: 51,
- allowance: '6006000000000000000000000000',
- period: 6048,
- renewWindow: 2248,
- download: 1099511627776,
- upload: 1100000000000,
- storage: 1000000000000,
- },
- })
- ).toEqual({
- set: 'autopilot',
- allowance: new BigNumber('6006'),
- amount: new BigNumber('51'),
- period: new BigNumber('6'),
- renewWindow: new BigNumber('2.2301587301587302'),
- download: new BigNumber('1.099511627776'),
- upload: new BigNumber('1.1'),
- storage: new BigNumber('1'),
- allowRedundantIPs: false,
- maxDowntimeHours: new BigNumber('1440'),
- defragThreshold: new BigNumber('1000'),
- })
- })
-
- it('up', () => {
- expect(
- transformUp({
- set: 'autopilot',
- allowance: new BigNumber('6006'),
- amount: new BigNumber('51'),
- period: new BigNumber('6'),
- renewWindow: new BigNumber('2.2301587301587302'),
- download: new BigNumber('1.099511627776'),
- upload: new BigNumber('1.1'),
- storage: new BigNumber('1'),
- allowRedundantIPs: false,
- maxDowntimeHours: new BigNumber('1440'),
- defragThreshold: new BigNumber('1000'),
- })
- ).toEqual({
- wallet: {
- defragThreshold: 1000,
- },
- hosts: {
- allowRedundantIPs: false,
- maxDowntimeHours: 1440,
- scoreOverrides: null,
- },
- contracts: {
- set: 'autopilot',
- amount: 51,
- allowance: '6006000000000000000000000000',
- period: 6048,
- renewWindow: 2248,
- download: 1099511627776,
- upload: 1100000000000,
- storage: 1000000000000,
- },
- })
- })
-
- it('accepts unknown values', () => {
- expect(
- transformUp(
- {
- set: 'autopilot',
- allowance: new BigNumber('6006'),
- amount: new BigNumber('51'),
- period: new BigNumber('6'),
- renewWindow: new BigNumber('2.2301587301587302'),
- download: new BigNumber('1.099511627776'),
- upload: new BigNumber('1.1'),
- storage: new BigNumber('1'),
- allowRedundantIPs: false,
- maxDowntimeHours: new BigNumber('1440'),
- defragThreshold: new BigNumber('1000'),
- },
- {
- foobar1: 'value',
- wallet: {
- foobar: 'value',
- },
- contracts: {
- foobar: 'value',
- period: 7777,
- },
- hosts: {
- foobar: 'value',
- },
- }
- )
- ).toEqual({
- foobar1: 'value',
- wallet: {
- foobar: 'value',
- defragThreshold: 1000,
- },
- hosts: {
- foobar: 'value',
- allowRedundantIPs: false,
- maxDowntimeHours: 1440,
- scoreOverrides: null,
- },
- contracts: {
- foobar: 'value',
- set: 'autopilot',
- amount: 51,
- allowance: '6006000000000000000000000000',
- period: 6048,
- renewWindow: 2248,
- download: 1099511627776,
- upload: 1100000000000,
- storage: 1000000000000,
- },
- })
- })
-})
diff --git a/apps/renterd/components/Autopilot/transform.ts b/apps/renterd/components/Autopilot/transform.ts
deleted file mode 100644
index cbce379fc..000000000
--- a/apps/renterd/components/Autopilot/transform.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import {
- blocksToWeeks,
- bytesToTB,
- TBToBytes,
- weeksToBlocks,
-} from '@siafoundation/design-system'
-import { AutopilotConfig } from '@siafoundation/react-renterd'
-import { toHastings, toSiacoins } from '@siafoundation/sia-js'
-import BigNumber from 'bignumber.js'
-import { scDecimalPlaces, SettingsData } from './fields'
-
-export function transformUp(
- values: SettingsData,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- existingValues?: any
-): AutopilotConfig {
- return {
- ...existingValues,
- contracts: {
- ...existingValues?.contracts,
- set: values.set,
- amount: Math.round(values.amount.toNumber()),
- allowance: toHastings(values.allowance).toString(),
- period: Math.round(weeksToBlocks(values.period.toNumber())),
- renewWindow: Math.round(weeksToBlocks(values.renewWindow.toNumber())),
- download: TBToBytes(values.download).toNumber(),
- upload: TBToBytes(values.upload).toNumber(),
- storage: TBToBytes(values.storage).toNumber(),
- },
- hosts: {
- ...existingValues?.hosts,
- maxDowntimeHours: values.maxDowntimeHours.toNumber(),
- allowRedundantIPs: values.allowRedundantIPs,
- scoreOverrides: existingValues?.hosts.scoreOverrides || null,
- },
- wallet: {
- ...existingValues?.wallet,
- defragThreshold: values.defragThreshold.toNumber(),
- },
- }
-}
-
-export function transformDown(config: AutopilotConfig): SettingsData {
- const set = config.contracts.set
- const allowance = toSiacoins(config.contracts.allowance, scDecimalPlaces)
- const amount = new BigNumber(config.contracts.amount)
- const period = new BigNumber(blocksToWeeks(config.contracts.period))
- const renewWindow = new BigNumber(blocksToWeeks(config.contracts.renewWindow))
- const download = bytesToTB(new BigNumber(config.contracts.download))
- const upload = bytesToTB(new BigNumber(config.contracts.upload))
- const storage = bytesToTB(new BigNumber(config.contracts.storage))
-
- return {
- // contracts
- set,
- allowance,
- amount,
- period,
- renewWindow,
- download,
- upload,
- storage,
- // hosts
- allowRedundantIPs: config.hosts.allowRedundantIPs,
- maxDowntimeHours: new BigNumber(config.hosts.maxDowntimeHours),
- // wallet
- defragThreshold: new BigNumber(config.wallet.defragThreshold),
- }
-}
diff --git a/apps/renterd/components/CmdRoot/AutopilotCmdGroup.tsx b/apps/renterd/components/CmdRoot/AutopilotCmdGroup.tsx
deleted file mode 100644
index 8e97a1292..000000000
--- a/apps/renterd/components/CmdRoot/AutopilotCmdGroup.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { routes } from '../../config/routes'
-import { useRouter } from 'next/router'
-import { useDialog } from '../../contexts/dialog'
-import { CommandGroup, CommandItemNav, CommandItemSearch } from './Item'
-import { Page } from './types'
-
-const commandPage = {
- namespace: 'autopilot',
- label: 'Autopilot',
-}
-
-type Props = {
- currentPage: Page
- parentPage?: Page
- pushPage: (page: Page) => void
-}
-
-export function AutopilotCmdGroup({
- parentPage,
- currentPage,
- pushPage,
-}: Props) {
- const router = useRouter()
- const { closeDialog } = useDialog()
- return (
-
- {
- pushPage(commandPage)
- }}
- >
- {commandPage.label}
-
- {
- router.push(routes.autopilot.index)
- closeDialog()
- }}
- >
- Open autopilot
-
- {
- router.push(routes.autopilot.contracts)
- closeDialog()
- }}
- >
- Configure contracts
-
- {
- router.push(routes.autopilot.hosts)
- closeDialog()
- }}
- >
- Configure hosts
-
- {
- router.push(routes.autopilot.wallet)
- closeDialog()
- }}
- >
- Configure wallet
-
-
- )
-}
diff --git a/apps/renterd/components/CmdRoot/ConfigCmdGroup.tsx b/apps/renterd/components/CmdRoot/ConfigCmdGroup.tsx
index 6f26208f9..37aaf79b6 100644
--- a/apps/renterd/components/CmdRoot/ConfigCmdGroup.tsx
+++ b/apps/renterd/components/CmdRoot/ConfigCmdGroup.tsx
@@ -3,6 +3,8 @@ import { useRouter } from 'next/router'
import { useDialog } from '../../contexts/dialog'
import { CommandGroup, CommandItemNav, CommandItemSearch } from './Item'
import { Page } from './types'
+import { useConfig } from '../../contexts/config'
+import { useApp } from '../../contexts/app'
const commandPage = {
namespace: 'configuration',
@@ -17,7 +19,9 @@ type Props = {
export function ConfigCmdGroup({ currentPage, parentPage, pushPage }: Props) {
const router = useRouter()
+ const { showAdvanced } = useConfig()
const { closeDialog } = useDialog()
+ const { autopilot } = useApp()
return (
Open configuration
+ {autopilot.status === 'on' && (
+ {
+ router.push(routes.config.storage)
+ closeDialog()
+ }}
+ >
+ Configure storage
+
+ )}
{
- router.push(routes.config.gouging)
+ router.push(routes.config.pricing)
closeDialog()
}}
>
- Configure gouging
-
- {
- router.push(routes.config.redundancy)
- closeDialog()
- }}
- >
- Configure redundancy
+ Configure pricing
+ {showAdvanced && (
+ <>
+ {autopilot.status === 'on' && (
+ <>
+ {
+ router.push(routes.config.hosts)
+ closeDialog()
+ }}
+ >
+ Configure hosts
+
+ {
+ router.push(routes.config.wallet)
+ closeDialog()
+ }}
+ >
+ Configure wallet
+
+ >
+ )}
+ {
+ router.push(routes.config.contracts)
+ closeDialog()
+ }}
+ >
+ Configure contracts
+
+ {
+ router.push(routes.config.uploads)
+ closeDialog()
+ }}
+ >
+ Configure uploads
+
+ {
+ router.push(routes.config.redundancy)
+ closeDialog()
+ }}
+ >
+ Configure redundancy
+
+ >
+ )}
)
}
diff --git a/apps/renterd/components/CmdRoot/index.tsx b/apps/renterd/components/CmdRoot/index.tsx
index 182a10054..324ad5868 100644
--- a/apps/renterd/components/CmdRoot/index.tsx
+++ b/apps/renterd/components/CmdRoot/index.tsx
@@ -10,7 +10,6 @@ import { WalletCmdGroup } from './WalletCmdGroup'
import { AppCmdGroup } from './AppCmdGroup'
import { useCallback, useState } from 'react'
import { NodeCmdGroup } from './NodeCmdGroup'
-import { AutopilotCmdGroup } from './AutopilotCmdGroup'
import { ConfigCmdGroup } from './ConfigCmdGroup'
import { useDialog } from '../../contexts/dialog'
import { useRouter } from 'next/router'
@@ -23,7 +22,6 @@ import { FilesCmd } from '../Files/FilesCmd'
import { useHosts } from '../../contexts/hosts'
import { useDebounce } from 'use-debounce'
import { CmdEmptyDefault } from './CmdEmpty'
-import { useApp } from '../../contexts/app'
type Props = {
panel?: boolean
@@ -32,7 +30,6 @@ type Props = {
export function CmdRoot({ panel }: Props) {
const { resetFilters: resetContractsFilters } = useContracts()
const { resetFilters: resetHostsFilters } = useHosts()
- const { autopilot } = useApp()
const { closeDialog } = useDialog()
const router = useRouter()
const [search, setSearch] = useState('')
@@ -105,9 +102,6 @@ export function CmdRoot({ panel }: Props) {
afterSelect()
}}
/>
- {autopilot.status === 'on' && (
-
- )}
+ {!!changeCount && (
+
+ {changeCount === 1 ? '1 change' : `${changeCount} changes`}
+
+ )}
+
+
+
+
+
+
+ Save changes
+
+
+
+
+ }
+ >
+
+
Options
+
+
setShouldSyncDefaultContractSet(val)}
+ >
+ sync default contract set
+
+
+ Automatically update the default contract set to be the same as
+ the autopilot contract set when changes are saved.
+
+
+
+
+
+
+ )
+}
diff --git a/apps/renterd/components/Config/ConfigNav.tsx b/apps/renterd/components/Config/ConfigNav.tsx
new file mode 100644
index 000000000..4616b9889
--- /dev/null
+++ b/apps/renterd/components/Config/ConfigNav.tsx
@@ -0,0 +1,22 @@
+import { Text, Switch, Tooltip } from '@siafoundation/design-system'
+import { useConfig } from '../../contexts/config'
+
+export function ConfigNav() {
+ const { showAdvanced, setShowAdvanced } = useConfig()
+
+ return (
+
+
+
+ setShowAdvanced(checked)}
+ />
+
+ Advanced
+
+
+
+
+ )
+}
diff --git a/apps/renterd/components/Config/ConfigStats.tsx b/apps/renterd/components/Config/ConfigStats.tsx
new file mode 100644
index 000000000..54085d6ce
--- /dev/null
+++ b/apps/renterd/components/Config/ConfigStats.tsx
@@ -0,0 +1,97 @@
+import {
+ Text,
+ TBToBytes,
+ ValueSc,
+ ValueNum,
+ useSiacoinFiat,
+} from '@siafoundation/design-system'
+import { humanBytes, toHastings } from '@siafoundation/sia-js'
+import { useConfig } from '../../contexts/config'
+import { useApp } from '../../contexts/app'
+
+export function AutopilotStats() {
+ const { autopilot } = useApp()
+ const {
+ canEstimate,
+ estimatedSpendingPerMonth,
+ estimatedSpendingPerTB,
+ redundancyMultiplier,
+ storageTB,
+ showAdvanced,
+ } = useConfig()
+ const perMonth = useSiacoinFiat({ sc: estimatedSpendingPerMonth })
+ const perTB = useSiacoinFiat({ sc: estimatedSpendingPerTB })
+
+ if (autopilot.status !== 'on') {
+ return null
+ }
+
+ return !canEstimate ? (
+
+ {showAdvanced
+ ? 'Enter expected storage, period, and allowance values to estimate monthly spending.'
+ : 'Enter expected storage and max price to estimate monthly spending.'}
+
+ ) : (
+
+
+ Estimate:
+
+
+
+ {perTB.fiat && (
+
+
+ `(${perTB.currency.prefix}${v.toFixed(perTB.currency.fixed)})`
+ }
+ />
+
+ )}
+
+ per TB/month with {redundancyMultiplier.toFixed(1)}x redundancy
+
+
+
+
+ {perMonth.fiat && (
+
+
+ `(${perMonth.currency.prefix}${v.toFixed(
+ perMonth.currency.fixed
+ )})`
+ }
+ />
+
+ )}
+
+ to store {humanBytes(TBToBytes(storageTB).toNumber())}/month with{' '}
+ {redundancyMultiplier.toFixed(1)}x redundancy
+
+
+
+ )
+}
diff --git a/apps/renterd/components/Config/fields.tsx b/apps/renterd/components/Config/fields.tsx
deleted file mode 100644
index 218f5c690..000000000
--- a/apps/renterd/components/Config/fields.tsx
+++ /dev/null
@@ -1,422 +0,0 @@
-/* eslint-disable react/no-unescaped-entities */
-import {
- Code,
- ConfigFields,
- FieldSwitch,
- hoursInDays,
- secondsInMinutes,
- Text,
- Tooltip,
-} from '@siafoundation/design-system'
-import BigNumber from 'bignumber.js'
-import React from 'react'
-
-export const scDecimalPlaces = 6
-
-export const defaultContractSet = {
- contractSet: '',
-}
-
-export const defaultUploadPacking = {
- uploadPackingEnabled: false,
-}
-
-export const defaultConfigApp = {
- includeRedundancyMaxStoragePrice: true,
- includeRedundancyMaxUploadPrice: true,
-}
-
-export const defaultGouging = {
- maxRpcPrice: undefined as BigNumber | undefined,
- maxStoragePrice: undefined as BigNumber | undefined,
- maxContractPrice: undefined as BigNumber | undefined,
- maxDownloadPrice: undefined as BigNumber | undefined,
- maxUploadPrice: undefined as BigNumber | undefined,
- minMaxCollateral: undefined as BigNumber | undefined,
- hostBlockHeightLeeway: undefined as BigNumber | undefined,
- minPriceTableValidity: undefined as BigNumber | undefined,
- minAccountExpiry: undefined as BigNumber | undefined,
- minMaxEphemeralAccountBalance: undefined as BigNumber | undefined,
-}
-
-export const defaultRedundancy = {
- minShards: undefined as BigNumber | undefined,
- totalShards: undefined as BigNumber | undefined,
-}
-
-export const defaultValues = {
- // contract set
- ...defaultContractSet,
- // upload packing
- ...defaultUploadPacking,
- // gouging
- ...defaultGouging,
- // redundancy
- ...defaultRedundancy,
- // config app
- ...defaultConfigApp,
-}
-
-export type ContractSetData = typeof defaultContractSet
-export type UploadPackingData = typeof defaultUploadPacking
-export type ConfigAppData = typeof defaultConfigApp
-export type GougingData = typeof defaultGouging
-export type RedundancyData = typeof defaultRedundancy
-export type SettingsData = typeof defaultValues
-
-type Categories = 'contractset' | 'uploadpacking' | 'gouging' | 'redundancy'
-
-type GetFields = {
- redundancyMultiplier: BigNumber
- includeRedundancyMaxStoragePrice: boolean
- includeRedundancyMaxUploadPrice: boolean
- storageAverage?: BigNumber
- uploadAverage?: BigNumber
- downloadAverage?: BigNumber
- contractAverage?: BigNumber
-}
-
-export function getFields({
- redundancyMultiplier,
- includeRedundancyMaxStoragePrice,
- includeRedundancyMaxUploadPrice,
- storageAverage,
- uploadAverage,
- downloadAverage,
- contractAverage,
-}: GetFields): ConfigFields {
- return {
- // contract
- contractSet: {
- category: 'contractset',
- type: 'text',
- title: 'Default contract set',
- placeholder: 'autopilot',
- suggestion: 'autopilot',
- suggestionTip: (
- <>
- Autopilot users will typically want to keep this the same as the
- autopilot contract set.
- >
- ),
- description: (
- <>The default contract set is where data is uploaded to by default.>
- ),
- validation: {
- required: 'required',
- },
- },
- uploadPackingEnabled: {
- category: 'uploadpacking',
- type: 'boolean',
- title: 'Upload packing',
- description: (
- <>
- Data on the Sia network is stored in 40MiB sectors so by default
- uploaded files are divided and padded into these 40MiB peices. This
- means that storage is wasted on padding but more importantly files
- smaller than 40MiB still use 40MiB of space. Upload packing avoids
- this waste by buffering files and packing them together before they
- are uploaded to the network. This trades some performance for storage
- efficiency. It is also important to note that because buffered files
- are temporarily stored on disk they must be considered when backing up
- your renterd data.
- >
- ),
- validation: {},
- },
- // gouging
- maxStoragePrice: {
- category: 'gouging',
- type: 'siacoin',
- title: 'Max storage price',
- description: <>The max allowed price to store 1 TB per month.>,
- units: 'SC/TB/month',
- average: storageAverage,
- averageTip: getAverageTip(
- includeRedundancyMaxStoragePrice,
- redundancyMultiplier
- ),
- after: function After({ form, fields }) {
- return (
-
-
-
-
- Including {redundancyMultiplier.toFixed(1)}x redundancy
-
-
-
-
- )
- },
- decimalsLimitSc: scDecimalPlaces,
- validation: {
- required: 'required',
- },
- },
- maxUploadPrice: {
- category: 'gouging',
- type: 'siacoin',
- title: 'Max upload price',
- description: <>The max allowed price to upload 1 TB.>,
- units: 'SC/TB/month',
- average: uploadAverage,
- averageTip: getAverageTip(
- includeRedundancyMaxUploadPrice,
- redundancyMultiplier
- ),
- after: function After({ form, fields }) {
- return (
-
-
-
-
- Including {redundancyMultiplier.toFixed(1)}x redundancy
-
-
-
-
- )
- },
- decimalsLimitSc: scDecimalPlaces,
- validation: {
- required: 'required',
- },
- },
- maxDownloadPrice: {
- category: 'gouging',
- type: 'siacoin',
- title: 'Max download price',
- description: <>The max allowed price to download 1 TB.>,
- units: 'SC/TB/month',
- average: downloadAverage,
- averageTip: `Averages provided by Sia Central.`,
- decimalsLimitSc: scDecimalPlaces,
- validation: {
- required: 'required',
- },
- },
- maxContractPrice: {
- category: 'gouging',
- type: 'siacoin',
- title: 'Max contract price',
- description: <>The max allowed price to form a contract.>,
- average: contractAverage,
- decimalsLimitSc: scDecimalPlaces,
- tipsDecimalsLimitSc: 3,
- validation: {
- required: 'required',
- },
- },
- maxRpcPrice: {
- category: 'gouging',
- type: 'siacoin',
- title: 'Max RPC price',
- description: (
- <>The max allowed base price for RPCs in siacoins per million calls.>
- ),
- units: 'SC/million',
- decimalsLimitSc: scDecimalPlaces,
- validation: {
- required: 'required',
- },
- },
- minMaxCollateral: {
- category: 'gouging',
- type: 'siacoin',
- title: 'Min max collateral',
- description: (
- <>The min value for max collateral in the host's price settings.>
- ),
- decimalsLimitSc: scDecimalPlaces,
- validation: {
- required: 'required',
- },
- },
- hostBlockHeightLeeway: {
- category: 'gouging',
- type: 'number',
- title: 'Block height leeway',
- description: (
- <>
- The amount of blocks of leeway given to the host block height in the
- host's price table.
- >
- ),
- units: 'blocks',
- decimalsLimit: 0,
- suggestion: new BigNumber(6),
- suggestionTip: 'The recommended value is 6 blocks.',
- validation: {
- required: 'required',
- validate: {
- min: (value) =>
- new BigNumber(value as BigNumber).gte(3) ||
- 'must be at least 3 blocks',
- },
- },
- },
- minPriceTableValidity: {
- category: 'gouging',
- type: 'number',
- title: 'Min price table validity',
- units: 'minutes',
- description: (
- <>The min accepted value for `Validity` in the host's price settings.>
- ),
- validation: {
- required: 'required',
- validate: {
- min: (value) =>
- new BigNumber(value as BigNumber).gte(secondsInMinutes(10)) ||
- 'must be at least 10 seconds',
- },
- },
- },
- minAccountExpiry: {
- category: 'gouging',
- type: 'number',
- title: 'Min account expiry',
- units: 'days',
- description: (
- <>
- The min accepted value for `AccountExpiry` in the host's price
- settings.
- >
- ),
- validation: {
- required: 'required',
- validate: {
- min: (value) =>
- new BigNumber(value as BigNumber).gte(hoursInDays(1)) ||
- 'must be at least 1 hour',
- },
- },
- },
- minMaxEphemeralAccountBalance: {
- category: 'gouging',
- type: 'siacoin',
- title: 'Min max ephemeral account balance',
- description: (
- <>
- The min accepted value for `MaxEphemeralAccountBalance` in the host's
- price settings.
- >
- ),
- decimalsLimitSc: scDecimalPlaces,
- validation: {
- required: 'required',
- validate: {
- min: (value) =>
- new BigNumber(value as BigNumber).gte(1) || 'must be at least 1 SC',
- },
- },
- },
-
- // Redundancy
- minShards: {
- type: 'number',
- category: 'redundancy',
- title: 'Min shards',
- description: <>The min amount of shards needed to reconstruct a slab.>,
- units: 'shards',
- validation: {
- required: 'required',
- validate: {
- min: (value) =>
- new BigNumber(value as BigNumber).gt(0) || 'must be greater than 0',
- },
- },
- trigger: ['totalShards'],
- },
- totalShards: {
- type: 'number',
- category: 'redundancy',
- title: 'Total shards',
- description: <>The total amount of shards for each slab.>,
- units: 'shards',
- validation: {
- required: 'required',
- validate: {
- gteMinShards: (value, values) =>
- new BigNumber(value as BigNumber).gte(values.minShards) ||
- 'must be at least equal to min shards',
- max: (value) =>
- new BigNumber(value as BigNumber).lt(256) ||
- 'must be less than 256',
- },
- },
- },
- // hidden fields used by other config options
- includeRedundancyMaxStoragePrice: {
- type: 'boolean',
- title: 'Include redundancy',
- validation: {},
- },
- includeRedundancyMaxUploadPrice: {
- type: 'boolean',
- title: 'Include redundancy',
- validation: {},
- },
- }
-}
-
-function getAverageTip(
- includeRedundancy: boolean,
- redundancyMultiplier: BigNumber
-) {
- if (includeRedundancy) {
- return `The average price is adjusted for ${redundancyMultiplier.toFixed(
- 1
- )}x redundancy. Averages provided by Sia Central.`
- }
- return `The average price is not adjusted for redundancy. Averages provided by Sia Central.`
-}
-
-function getRedundancyTip(
- includeRedundancy: boolean,
- redundancyMultiplier: BigNumber
-) {
- if (includeRedundancy) {
- return (
-
-
- Specified max price includes the cost of{' '}
- {redundancyMultiplier.toFixed(1)}x redundancy.
-
-
- Redundancy is calculated from the ratio of data shards:{' '}
- min shards / total shards
.
-
-
- )
- }
- return `Specified max price does not include redundancy.`
-}
diff --git a/apps/renterd/components/Config/index.tsx b/apps/renterd/components/Config/index.tsx
index 2a18a739a..010ae12da 100644
--- a/apps/renterd/components/Config/index.tsx
+++ b/apps/renterd/components/Config/index.tsx
@@ -1,387 +1,61 @@
-import {
- Text,
- Button,
- triggerSuccessToast,
- triggerErrorToast,
- ConfigurationPanel,
- TBToBytes,
- monthsToBlocks,
- useOnInvalid,
-} from '@siafoundation/design-system'
-import { Reset16, Save16 } from '@siafoundation/react-icons'
-import { useCallback, useEffect, useMemo, useState } from 'react'
+import { ConfigurationPanel } from '@siafoundation/design-system'
import { RenterdSidenav } from '../RenterdSidenav'
import { routes } from '../../config/routes'
import { useDialog } from '../../contexts/dialog'
-import { RenterdAuthedLayout } from '../../components/RenterdAuthedLayout'
-import {
- ContractSetSettings,
- GougingSettings,
- RedundancySettings,
- UploadPackingSettings,
- useSettingUpdate,
-} from '@siafoundation/react-renterd'
-import { defaultValues, getFields } from './fields'
-import {
- getRedundancyMultiplier,
- getRedundancyMultiplierIfIncluded,
- transformDown,
- transformUpConfigApp,
- transformUpContractSet,
- transformUpGouging,
- transformUpRedundancy,
- transformUpUploadPacking,
-} from './transform'
-import { useForm } from 'react-hook-form'
-import { toSiacoins } from '@siafoundation/sia-js'
-import { useContractSetSettings } from '../../hooks/useContractSetSettings'
-import { useGougingSettings } from '../../hooks/useGougingSettings'
-import { useRedundancySettings } from '../../hooks/useRedundancySettings'
-import {
- ConfigDisplayOptions,
- configDisplayOptionsKey,
- useConfigDisplayOptions,
-} from '../../hooks/useConfigDisplayOptions'
-import { useUploadPackingSettings } from '../../hooks/useUploadPackingSettings'
-import { useSiaCentralHostsNetworkAverages } from '@siafoundation/react-sia-central'
+import { RenterdAuthedLayout } from '../RenterdAuthedLayout'
+import { useConfig } from '../../contexts/config'
+import { AutopilotStats } from './ConfigStats'
+import { ConfigActions } from './ConfigActions'
+import { ConfigNav } from './ConfigNav'
export function Config() {
const { openDialog } = useDialog()
- // settings that 404 when empty
- const contractSet = useContractSetSettings({
- config: {
- swr: {
- // Do not automatically refetch
- revalidateOnFocus: false,
- errorRetryCount: 0,
- },
- },
- })
- const configApp = useConfigDisplayOptions({
- config: {
- swr: {
- // Do not automatically refetch
- revalidateOnFocus: false,
- errorRetryCount: 0,
- },
- },
- })
- // settings with initial defaults
- const gouging = useGougingSettings({
- config: {
- swr: {
- // Do not automatically refetch
- revalidateOnFocus: false,
- },
- },
- })
- const redundancy = useRedundancySettings({
- config: {
- swr: {
- // Do not automatically refetch
- revalidateOnFocus: false,
- },
- },
- })
- const uploadPacking = useUploadPackingSettings({
- config: {
- swr: {
- // Do not automatically refetch
- revalidateOnFocus: false,
- },
- },
- })
-
- const settingUpdate = useSettingUpdate()
-
- const averages = useSiaCentralHostsNetworkAverages({
- config: {
- swr: {
- revalidateOnFocus: false,
- },
- },
- })
-
- const form = useForm({
- mode: 'all',
- defaultValues,
- })
-
- const resetFormData = useCallback(
- (
- contractSetData: ContractSetSettings | undefined,
- uploadPackingData: UploadPackingSettings,
- gougingData: GougingSettings,
- redundancyData: RedundancySettings,
- configAppData: ConfigDisplayOptions | undefined
- ) => {
- form.reset(
- transformDown(
- contractSetData,
- uploadPackingData,
- gougingData,
- redundancyData,
- configAppData
- )
- )
- },
- [form]
- )
-
- // init - when new config is fetched, set the form
- const [hasInit, setHasInit] = useState(false)
- useEffect(() => {
- if (
- gouging.data &&
- redundancy.data &&
- uploadPacking.data &&
- (contractSet.data || contractSet.error) &&
- (configApp.data || configApp.error) &&
- !hasInit
- ) {
- resetFormData(
- contractSet.data,
- uploadPacking.data,
- gouging.data,
- redundancy.data,
- configApp.data
- )
- setHasInit(true)
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- contractSet.data,
- contractSet.error,
- uploadPacking.data,
- gouging.data,
- redundancy.data,
- configApp.data,
- configApp.error,
- ])
-
- const reset = useCallback(async () => {
- const contractSetData = await contractSet.mutate()
- const gougingData = await gouging.mutate()
- const redundancyData = await redundancy.mutate()
- const uploadPackingData = await uploadPacking.mutate()
- const configAppData = await configApp.mutate()
- if (!gougingData || !redundancyData) {
- triggerErrorToast('Error fetching settings.')
- } else {
- resetFormData(
- contractSetData,
- uploadPackingData,
- gougingData,
- redundancyData,
- configAppData
- )
- }
- }, [
- contractSet,
- gouging,
- uploadPacking,
- redundancy,
- configApp,
- resetFormData,
- ])
-
- const minShards = form.watch('minShards')
- const totalShards = form.watch('totalShards')
- const includeRedundancyMaxStoragePrice = form.watch(
- 'includeRedundancyMaxStoragePrice'
- )
- const includeRedundancyMaxUploadPrice = form.watch(
- 'includeRedundancyMaxUploadPrice'
- )
-
- const fields = useMemo(() => {
- const redundancyMultiplier = getRedundancyMultiplier(minShards, totalShards)
- if (averages.data) {
- return getFields({
- redundancyMultiplier,
- includeRedundancyMaxStoragePrice,
- includeRedundancyMaxUploadPrice,
- storageAverage: toSiacoins(averages.data.settings.storage_price) // bytes/block
- .times(monthsToBlocks(1)) // bytes/month
- .times(TBToBytes(1)) // TB/month
- .times(
- getRedundancyMultiplierIfIncluded(
- minShards,
- totalShards,
- includeRedundancyMaxStoragePrice
- )
- ), // redundancy
- uploadAverage: toSiacoins(averages.data.settings.upload_price) // bytes
- .times(TBToBytes(1)) // TB
- .times(
- getRedundancyMultiplierIfIncluded(
- minShards,
- totalShards,
- includeRedundancyMaxUploadPrice
- )
- ), // redundancy
- downloadAverage: toSiacoins(averages.data.settings.download_price) // bytes
- .times(TBToBytes(1)), // TB
- contractAverage: toSiacoins(averages.data.settings.contract_price),
- })
- }
- return getFields({
- redundancyMultiplier,
- includeRedundancyMaxStoragePrice,
- includeRedundancyMaxUploadPrice,
- })
- }, [
- averages.data,
- minShards,
- totalShards,
- includeRedundancyMaxStoragePrice,
- includeRedundancyMaxUploadPrice,
- ])
-
- const onValid = useCallback(
- async (values: typeof defaultValues) => {
- if (!gouging.data || !redundancy.data) {
- return
- }
- try {
- const contractSetResponse = await settingUpdate.put({
- params: {
- key: 'contractset',
- },
- payload: transformUpContractSet(
- values,
- contractSet.data as Record
- ),
- })
- const uploadPackingResponse = await settingUpdate.put({
- params: {
- key: 'uploadpacking',
- },
- payload: transformUpUploadPacking(
- values,
- uploadPacking.data as Record
- ),
- })
- const gougingResponse = await settingUpdate.put({
- params: {
- key: 'gouging',
- },
- payload: transformUpGouging(
- values,
- gouging.data as Record
- ),
- })
- const redundancyResponse = await settingUpdate.put({
- params: {
- key: 'redundancy',
- },
- payload: transformUpRedundancy(
- values,
- redundancy.data as Record
- ),
- })
- const configAppResponse = await settingUpdate.put({
- params: {
- key: configDisplayOptionsKey,
- },
- payload: transformUpConfigApp(
- values,
- configApp.data as Record
- ),
- })
- if (contractSetResponse.error) {
- throw Error(contractSetResponse.error)
- }
- if (uploadPackingResponse.error) {
- throw Error(uploadPackingResponse.error)
- }
- if (gougingResponse.error) {
- throw Error(gougingResponse.error)
- }
- if (redundancyResponse.error) {
- throw Error(redundancyResponse.error)
- }
- if (configAppResponse.error) {
- throw Error(configAppResponse.error)
- }
- triggerSuccessToast('Configuration has been saved.')
- reset()
- } catch (e) {
- triggerErrorToast((e as Error).message)
- console.log(e)
- }
- },
- [
- settingUpdate,
- contractSet,
- uploadPacking,
- redundancy,
- gouging,
- configApp,
- reset,
- ]
- )
-
- const onInvalid = useOnInvalid(fields)
-
- const onSubmit = useMemo(
- () => form.handleSubmit(onValid, onInvalid),
- [form, onValid, onInvalid]
- )
-
- const changeCount = Object.entries(form.formState.dirtyFields).filter(
- ([_, val]) => !!val
- ).length
+ const { form, fields } = useConfig()
return (
}
sidenav={ }
- actions={
-
- {!!changeCount && (
-
- {changeCount === 1 ? '1 change' : `${changeCount} changes`}
-
- )}
-
-
-
-
-
- Save changes
-
-
- }
+ stats={ }
+ actions={ }
openSettings={() => openDialog('settings')}
>
-
+
+
+
+
diff --git a/apps/renterd/components/Config/transform.spec.ts b/apps/renterd/components/Config/transform.spec.ts
deleted file mode 100644
index e247448e5..000000000
--- a/apps/renterd/components/Config/transform.spec.ts
+++ /dev/null
@@ -1,219 +0,0 @@
-import BigNumber from 'bignumber.js'
-import {
- transformDown,
- transformUpContractSet,
- transformUpGouging,
- transformUpRedundancy,
-} from './transform'
-
-describe('data transforms', () => {
- it('down', () => {
- expect(
- transformDown(
- { default: 'myset' },
- { enabled: true },
- {
- hostBlockHeightLeeway: 4,
- maxContractPrice: '20000000000000000000000000',
- maxDownloadPrice: '1004310000000000000000000000',
- maxRPCPrice: '99970619000000000000000000',
- maxStoragePrice: '210531181019',
- maxUploadPrice: '1000232323000000000000000000',
- minAccountExpiry: 86400000000000,
- minMaxCollateral: '10000000000000000000000000',
- minMaxEphemeralAccountBalance: '1000000000000000000000000',
- minPriceTableValidity: 300000000000,
- },
- {
- minShards: 10,
- totalShards: 30,
- },
- {
- includeRedundancyMaxStoragePrice: false,
- includeRedundancyMaxUploadPrice: false,
- }
- )
- ).toEqual({
- contractSet: 'myset',
- uploadPackingEnabled: true,
- hostBlockHeightLeeway: new BigNumber(4),
- maxContractPrice: new BigNumber('20'),
- maxDownloadPrice: new BigNumber('1004.31'),
- maxRpcPrice: new BigNumber('99970619'),
- maxStoragePrice: new BigNumber('909.494702'),
- maxUploadPrice: new BigNumber('1000.232323'),
- minAccountExpiry: new BigNumber(1),
- minMaxCollateral: new BigNumber('10'),
- minMaxEphemeralAccountBalance: new BigNumber('1'),
- minPriceTableValidity: new BigNumber(5),
- minShards: new BigNumber(10),
- totalShards: new BigNumber(30),
- includeRedundancyMaxStoragePrice: false,
- includeRedundancyMaxUploadPrice: false,
- })
- })
-
- it('down with include redundancy for storage and upload', () => {
- expect(
- transformDown(
- { default: 'myset' },
- { enabled: true },
- {
- hostBlockHeightLeeway: 4,
- maxContractPrice: '20000000000000000000000000',
- maxDownloadPrice: '1004310000000000000000000000',
- maxRPCPrice: '99970619000000000000000000',
- maxStoragePrice: '210531181019',
- maxUploadPrice: '1000232323000000000000000000',
- minAccountExpiry: 86400000000000,
- minMaxCollateral: '10000000000000000000000000',
- minMaxEphemeralAccountBalance: '1000000000000000000000000',
- minPriceTableValidity: 300000000000,
- },
- {
- minShards: 10,
- totalShards: 30,
- },
- {
- includeRedundancyMaxStoragePrice: true,
- includeRedundancyMaxUploadPrice: true,
- }
- )
- ).toEqual({
- contractSet: 'myset',
- uploadPackingEnabled: true,
- hostBlockHeightLeeway: new BigNumber(4),
- maxContractPrice: new BigNumber('20'),
- maxDownloadPrice: new BigNumber('1004.31'),
- maxRpcPrice: new BigNumber('99970619'),
- maxStoragePrice: new BigNumber('2728.484106'),
- maxUploadPrice: new BigNumber('3000.696969'),
- minAccountExpiry: new BigNumber(1),
- minMaxCollateral: new BigNumber('10'),
- minMaxEphemeralAccountBalance: new BigNumber('1'),
- minPriceTableValidity: new BigNumber(5),
- minShards: new BigNumber(10),
- totalShards: new BigNumber(30),
- includeRedundancyMaxStoragePrice: true,
- includeRedundancyMaxUploadPrice: true,
- })
- })
-
- it('up contractset', () => {
- expect(
- transformUpContractSet(
- {
- contractSet: 'myset',
- },
- {
- default: '77777777777',
- foobar: 'value',
- }
- )
- ).toEqual({
- default: 'myset',
- foobar: 'value',
- })
- })
-
- it('up gouging', () => {
- expect(
- transformUpGouging(
- {
- contractSet: 'myset',
- uploadPackingEnabled: false,
- hostBlockHeightLeeway: new BigNumber(4),
- maxContractPrice: new BigNumber('20'),
- maxDownloadPrice: new BigNumber('1004.31'),
- maxRpcPrice: new BigNumber('99970619'),
- maxStoragePrice: new BigNumber('909.494702'),
- maxUploadPrice: new BigNumber('1000.232323'),
- minAccountExpiry: new BigNumber(1),
- minMaxCollateral: new BigNumber('10'),
- minMaxEphemeralAccountBalance: new BigNumber('1'),
- minPriceTableValidity: new BigNumber(5),
- minShards: new BigNumber(10),
- totalShards: new BigNumber(30),
- includeRedundancyMaxStoragePrice: false,
- includeRedundancyMaxUploadPrice: false,
- },
- {
- maxStoragePrice: '77777777777',
- foobar: 'value',
- }
- )
- ).toEqual({
- foobar: 'value',
- hostBlockHeightLeeway: 4,
- maxContractPrice: '20000000000000000000000000',
- maxDownloadPrice: '1004310000000000000000000000',
- maxRPCPrice: '99970619000000000000000000',
- maxStoragePrice: '210531181019',
- maxUploadPrice: '1000232323000000000000000000',
- minAccountExpiry: 86400000000000,
- minMaxCollateral: '10000000000000000000000000',
- minMaxEphemeralAccountBalance: '1000000000000000000000000',
- minPriceTableValidity: 300000000000,
- })
- })
-
- it('up gouging with include redundancy for storage', () => {
- expect(
- transformUpGouging(
- {
- contractSet: 'myset',
- uploadPackingEnabled: false,
- hostBlockHeightLeeway: new BigNumber(4),
- maxContractPrice: new BigNumber('20'),
- maxDownloadPrice: new BigNumber('1004.31'),
- maxRpcPrice: new BigNumber('99970619'),
- maxStoragePrice: new BigNumber('909.494702'),
- maxUploadPrice: new BigNumber('1000.232323'),
- minAccountExpiry: new BigNumber(1),
- minMaxCollateral: new BigNumber('10'),
- minMaxEphemeralAccountBalance: new BigNumber('1'),
- minPriceTableValidity: new BigNumber(5),
- minShards: new BigNumber(10),
- totalShards: new BigNumber(30),
- includeRedundancyMaxStoragePrice: true,
- includeRedundancyMaxUploadPrice: false,
- },
- {
- maxStoragePrice: '77777777777',
- foobar: 'value',
- }
- )
- ).toEqual({
- foobar: 'value',
- hostBlockHeightLeeway: 4,
- maxContractPrice: '20000000000000000000000000',
- maxDownloadPrice: '1004310000000000000000000000',
- maxRPCPrice: '99970619000000000000000000',
- maxStoragePrice: '70177060340',
- maxUploadPrice: '1000232323000000000000000000',
- minAccountExpiry: 86400000000000,
- minMaxCollateral: '10000000000000000000000000',
- minMaxEphemeralAccountBalance: '1000000000000000000000000',
- minPriceTableValidity: 300000000000,
- })
- })
-
- it('up redundancy', () => {
- expect(
- transformUpRedundancy(
- {
- minShards: new BigNumber(10),
- totalShards: new BigNumber(30),
- },
- {
- minShards: 77,
- foobar: 'value',
- }
- )
- ).toEqual({
- foobar: 'value',
- minShards: 10,
- totalShards: 30,
- })
- })
-})
diff --git a/apps/renterd/components/Files/EmptyState/index.tsx b/apps/renterd/components/Files/EmptyState/index.tsx
index 12fc37c89..96c6c20ee 100644
--- a/apps/renterd/components/Files/EmptyState/index.tsx
+++ b/apps/renterd/components/Files/EmptyState/index.tsx
@@ -39,7 +39,7 @@ export function EmptyState() {
finds contracts with hosts based on the settings you choose.
Autopilot also repairs your data as hosts come and go.
-
+
Configure autopilot →
diff --git a/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx b/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx
index 1061f0dc4..4d4ca657c 100644
--- a/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx
+++ b/apps/renterd/components/Files/FilesStatsMenu/FilesStatsMenuWarnings.tsx
@@ -86,12 +86,12 @@ export function FilesStatsMenuWarnings() {
Configure autopilot to get started.{' '}
Autopilot →
diff --git a/apps/renterd/components/Files/checks/useNotEnoughContracts.tsx b/apps/renterd/components/Files/checks/useNotEnoughContracts.tsx
index 1149c67ea..ef972a8d5 100644
--- a/apps/renterd/components/Files/checks/useNotEnoughContracts.tsx
+++ b/apps/renterd/components/Files/checks/useNotEnoughContracts.tsx
@@ -2,24 +2,18 @@ import { useContracts } from '../../../contexts/contracts'
import { useRedundancySettings } from '../../../hooks/useRedundancySettings'
export function useNotEnoughContracts() {
- const redundancy = useRedundancySettings({
- config: {
- swr: {
- // Do not automatically refetch
- revalidateOnFocus: false,
- },
- },
- })
- const { datasetCount, isLoading: isContractsLoading } = useContracts()
+ const redundancy = useRedundancySettings()
+ const { datasetConfirmedCount, isLoading: isContractsLoading } =
+ useContracts()
const active =
redundancy.data &&
!isContractsLoading &&
- datasetCount < redundancy.data.totalShards
+ datasetConfirmedCount < redundancy.data.totalShards
return {
active,
- count: datasetCount,
+ count: datasetConfirmedCount,
required: redundancy.data?.totalShards || 0,
}
}
diff --git a/apps/renterd/components/OnboardingBar.tsx b/apps/renterd/components/OnboardingBar.tsx
new file mode 100644
index 000000000..0f17df6ea
--- /dev/null
+++ b/apps/renterd/components/OnboardingBar.tsx
@@ -0,0 +1,269 @@
+import {
+ Button,
+ Link,
+ Logo,
+ Panel,
+ ScrollArea,
+ Text,
+ Tooltip,
+} from '@siafoundation/design-system'
+import {
+ RadioButton16,
+ CheckmarkFilled16,
+ Launch16,
+ PendingFilled16,
+ Subtract24,
+} from '@siafoundation/react-icons'
+import { useState } from 'react'
+import { useApp } from '../contexts/app'
+import { useSyncStatus } from '../hooks/useSyncStatus'
+import { routes } from '../config/routes'
+import { useDialog } from '../contexts/dialog'
+import { useNotEnoughContracts } from './Files/checks/useNotEnoughContracts'
+import { useAutopilotConfig, useWallet } from '@siafoundation/react-renterd'
+import BigNumber from 'bignumber.js'
+import { humanSiacoin } from '@siafoundation/sia-js'
+import { useAppSettings } from '@siafoundation/react-core'
+
+export function OnboardingBar() {
+ const { isUnlocked } = useAppSettings()
+ const app = useApp()
+ const { openDialog } = useDialog()
+ const wallet = useWallet()
+ const autopilot = useAutopilotConfig({
+ config: {
+ swr: {
+ errorRetryInterval: 10_000,
+ },
+ },
+ })
+ const [maximized, setMaximized] = useState(true)
+
+ const syncStatus = useSyncStatus()
+ const notEnoughContracts = useNotEnoughContracts()
+
+ if (!isUnlocked || app.autopilot.status !== 'on') {
+ return null
+ }
+
+ const walletBalance = new BigNumber(wallet.data?.confirmed || 0)
+ const allowance = new BigNumber(autopilot.data?.contracts.allowance || 0)
+
+ const step1Configured = app.autopilot.state.data?.configured
+ const step2Synced = syncStatus.isSynced
+ const step3Funded =
+ app.autopilot.state.data?.configured &&
+ wallet.data &&
+ walletBalance.gte(allowance)
+ const step4Contracts = !notEnoughContracts.active
+ const steps = [step1Configured, step2Synced, step3Funded, step4Contracts]
+ const totalSteps = steps.length
+ const completedSteps = steps.filter((step) => step).length
+
+ if (totalSteps === completedSteps) {
+ return null
+ }
+
+ if (maximized) {
+ return (
+
+
+
+
+
+
+
+ Welcome to Sia
+
+
+
setMaximized(false)}>
+
+
+
+
+
+ Get set up by completing the following steps. Once they are
+ complete, you can start uploading files.
+
+
+
+ Step 1: Configure your storage settings
+
+ }
+ description={
+ 'Specify how much data you plan to store and your target price.'
+ }
+ action={
+ step1Configured ? (
+
+
+
+ ) : (
+ <>
+
+
+
+
+
+
+ >
+ )
+ }
+ />
+
+ Step 2: Wait for the blockchain to sync
+
+ }
+ description={
+ 'The blockchain will sync in the background, this takes some time. No user action required.'
+ }
+ action={
+ step2Synced ? (
+
+
+
+ ) : (
+ <>
+
+ {syncStatus.syncPercent}%
+
+
+
+
+ >
+ )
+ }
+ />
+ openDialog('addressDetails')}
+ ellipsis
+ size="14"
+ underline="hover"
+ >
+ Step 3: Fund your wallet
+
+ }
+ description={`Fund your wallet with at least ${humanSiacoin(
+ allowance
+ )} siacoin to cover the required allowance.${
+ syncStatus.isWalletSynced
+ ? ''
+ : ' Balance will not be accurate until wallet is finished scanning.'
+ }`}
+ action={
+ step3Funded ? (
+
+
+
+ ) : (
+ <>
+
+ {syncStatus.walletScanPercent}%
+
+ openDialog('addressDetails')}
+ >
+
+
+
+
+
+ >
+ )
+ }
+ />
+
+ Step 4: Wait for storage contracts to form
+
+ }
+ description={
+ 'Once all other steps are complete, contracts will automatically form. No user action required.'
+ }
+ action={
+ step4Contracts ? (
+
+
+
+ ) : (
+ <>
+
+ {notEnoughContracts.count}/{notEnoughContracts.required}
+
+
+
+
+ >
+ )
+ }
+ />
+
+
+
+ )
+ }
+ return (
+
+ setMaximized(true)}
+ size="large"
+ className="flex gap-3 !px-3"
+ >
+
+
+ Setup: {completedSteps}/{totalSteps} steps complete
+
+
+
+ )
+}
+
+type SectionProps = {
+ title: React.ReactNode
+ action: React.ReactNode
+ description: React.ReactNode
+}
+
+function Section({ title, action, description }: SectionProps) {
+ return (
+
+
+
+
+
+ {description}
+
+
+
+
+ )
+}
diff --git a/apps/renterd/components/RenterdSidenav.tsx b/apps/renterd/components/RenterdSidenav.tsx
index 5bb428ba5..02e84faea 100644
--- a/apps/renterd/components/RenterdSidenav.tsx
+++ b/apps/renterd/components/RenterdSidenav.tsx
@@ -4,17 +4,14 @@ import {
FolderIcon,
FileContractIcon,
BarsProgressIcon,
- PlaneIcon,
BellIcon,
} from '@siafoundation/react-icons'
import { useAlerts } from '@siafoundation/react-renterd'
import { cx } from 'class-variance-authority'
import { routes } from '../config/routes'
-import { useApp } from '../contexts/app'
import { useDialog } from '../contexts/dialog'
export function RenterdSidenav() {
- const { autopilot } = useApp()
const alerts = useAlerts()
const { openDialog } = useDialog()
@@ -28,11 +25,6 @@ export function RenterdSidenav() {
- {autopilot.status === 'on' && (
-
-
-
- )}
diff --git a/apps/renterd/components/TransfersBar.tsx b/apps/renterd/components/TransfersBar.tsx
index 172d02b0b..fcd91a4ab 100644
--- a/apps/renterd/components/TransfersBar.tsx
+++ b/apps/renterd/components/TransfersBar.tsx
@@ -14,12 +14,14 @@ import {
} from '@siafoundation/react-icons'
import { useState } from 'react'
import { useFiles } from '../contexts/files'
+import { useAppSettings } from '@siafoundation/react-core'
function getProgress(transfer: { loaded?: number; size?: number }) {
return transfer.loaded !== undefined ? transfer.loaded / transfer.size : 1
}
export function TransfersBar() {
+ const { isUnlocked } = useAppSettings()
const { uploadsList, uploadCancel, downloadsList, downloadCancel } =
useFiles()
const [maximized, setMaximized] = useState(true)
@@ -27,6 +29,10 @@ export function TransfersBar() {
const uploadCount = uploadsList.length
const downloadCount = downloadsList.length
+ if (!isUnlocked) {
+ return null
+ }
+
if (uploadCount === 0 && downloadCount === 0) {
return null
}
diff --git a/apps/renterd/config/providers.tsx b/apps/renterd/config/providers.tsx
index 06b0d67bd..ae8fc59a9 100644
--- a/apps/renterd/config/providers.tsx
+++ b/apps/renterd/config/providers.tsx
@@ -4,6 +4,7 @@ import { ContractsProvider } from '../contexts/contracts'
import { HostsProvider } from '../contexts/hosts'
import React from 'react'
import { AppProvider } from '../contexts/app'
+import { ConfigProvider } from '../contexts/config'
type Props = {
children: React.ReactNode
@@ -12,18 +13,20 @@ type Props = {
export function Providers({ children }: Props) {
return (
-
-
-
-
- {/* this is here so that dialogs can use all the other providers,
+
+
+
+
+
+ {/* this is here so that dialogs can use all the other providers,
and the other providers can trigger dialogs */}
-
- {children}
-
-
-
-
+
+ {children}
+
+
+
+
+
)
}
diff --git a/apps/renterd/config/routes.ts b/apps/renterd/config/routes.ts
index 258dd52aa..474b59b54 100644
--- a/apps/renterd/config/routes.ts
+++ b/apps/renterd/config/routes.ts
@@ -5,15 +5,14 @@ export const routes = {
files: {
index: '/files',
},
- autopilot: {
- index: '/autopilot',
- contracts: '/autopilot#contracts',
- hosts: '/autopilot#hosts',
- wallet: '/autopilot#wallet',
- },
config: {
index: '/config',
- gouging: '/config#gouging',
+ storage: '/config#storage',
+ pricing: '/config#pricing',
+ hosts: '/config#hosts',
+ wallet: '/config#wallet',
+ contracts: '/config#contracts',
+ uploads: '/config#uploads',
redundancy: '/config#redundancy',
},
contracts: {
diff --git a/apps/renterd/contexts/config/fields.tsx b/apps/renterd/contexts/config/fields.tsx
new file mode 100644
index 000000000..c0a62f76b
--- /dev/null
+++ b/apps/renterd/contexts/config/fields.tsx
@@ -0,0 +1,643 @@
+/* eslint-disable react/no-unescaped-entities */
+import {
+ Code,
+ ConfigFields,
+ FieldSwitch,
+ Text,
+ Tooltip,
+ hoursInDays,
+ secondsInMinutes,
+ toFixedMax,
+} from '@siafoundation/design-system'
+import BigNumber from 'bignumber.js'
+import React from 'react'
+import {
+ defaultValues,
+ advancedDefaultAutopilot,
+ advancedDefaultContractSet,
+ advancedDefaultGouging,
+} from './types'
+
+export const scDecimalPlaces = 6
+
+type Categories =
+ | 'storage'
+ | 'gouging'
+ | 'hosts'
+ | 'wallet'
+ | 'contractset'
+ | 'uploadpacking'
+ | 'redundancy'
+
+type GetFields = {
+ isAutopilotEnabled: boolean
+ showAdvanced: boolean
+ redundancyMultiplier: BigNumber
+ includeRedundancyMaxStoragePrice: boolean
+ includeRedundancyMaxUploadPrice: boolean
+ storageAverage?: BigNumber
+ uploadAverage?: BigNumber
+ downloadAverage?: BigNumber
+ contractAverage?: BigNumber
+}
+
+export function getFields({
+ isAutopilotEnabled,
+ showAdvanced,
+ redundancyMultiplier,
+ includeRedundancyMaxStoragePrice,
+ includeRedundancyMaxUploadPrice,
+ storageAverage,
+ uploadAverage,
+ downloadAverage,
+ contractAverage,
+}: GetFields): ConfigFields {
+ return {
+ // storage
+ storageTB: {
+ type: 'number',
+ category: 'storage',
+ title: 'Expected storage',
+ description: <>The amount of storage you would like to rent in TB.>,
+ units: 'TB',
+ hidden: !isAutopilotEnabled,
+ validation: isAutopilotEnabled
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+ uploadTBMonth: {
+ type: 'number',
+ category: 'storage',
+ title: 'Expected upload',
+ description: (
+ <>The amount of upload bandwidth you plan to use each month in TB.>
+ ),
+ units: 'TB/month',
+ hidden: !isAutopilotEnabled || !showAdvanced,
+ validation:
+ isAutopilotEnabled && showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+ downloadTBMonth: {
+ type: 'number',
+ category: 'storage',
+ title: 'Expected download',
+ description: (
+ <>The amount of download bandwidth you plan to use each month in TB.>
+ ),
+ units: 'TB/month',
+ hidden: !isAutopilotEnabled || !showAdvanced,
+ validation:
+ isAutopilotEnabled && showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+ allowanceMonth: {
+ type: 'siacoin',
+ category: 'storage',
+ title: 'Allowance',
+ description: (
+ <>The amount of Siacoin you would like to spend for the period.>
+ ),
+ decimalsLimitSc: scDecimalPlaces,
+ hidden: !isAutopilotEnabled || !showAdvanced,
+ // always required, but set in background unless advanced mode
+ validation: {
+ required: 'required',
+ },
+ },
+ periodWeeks: {
+ type: 'number',
+ category: 'storage',
+ title: 'Period',
+ description: <>The length of the storage contracts.>,
+ units: 'weeks',
+ suggestion: advancedDefaultAutopilot.periodWeeks,
+ suggestionTip: `Typically ${advancedDefaultAutopilot.periodWeeks} weeks.`,
+ hidden: !isAutopilotEnabled || !showAdvanced,
+ validation:
+ isAutopilotEnabled && showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+ renewWindowWeeks: {
+ type: 'number',
+ category: 'storage',
+ title: 'Renew window',
+ description: (
+ <>
+ The number of weeks prior to contract expiration that Sia will attempt
+ to renew your contracts.
+ >
+ ),
+ units: 'weeks',
+ decimalsLimit: 6,
+ suggestion: advancedDefaultAutopilot.renewWindowWeeks,
+ suggestionTip: `Typically ${advancedDefaultAutopilot.renewWindowWeeks} weeks.`,
+ hidden: !isAutopilotEnabled || !showAdvanced,
+ validation:
+ isAutopilotEnabled && showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+ amountHosts: {
+ type: 'number',
+ category: 'storage',
+ title: 'Hosts',
+ description: <>The number of hosts to create contracts with.>,
+ units: 'hosts',
+ decimalsLimit: 0,
+ suggestion: advancedDefaultAutopilot.amountHosts,
+ suggestionTip: `Typically ${advancedDefaultAutopilot.amountHosts} hosts.`,
+ hidden: !isAutopilotEnabled || !showAdvanced,
+ validation:
+ isAutopilotEnabled && showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+ autopilotContractSet: {
+ type: 'text',
+ category: 'storage',
+ title: 'Contract set',
+ description: (
+ <>
+ The contract set that autopilot should use. This should typically be
+ the same as the default contract set.
+ >
+ ),
+ placeholder: advancedDefaultAutopilot.autopilotContractSet,
+ suggestion: advancedDefaultAutopilot.autopilotContractSet,
+ suggestionTip: (
+ <>
+ The default contract set is{' '}
+ {advancedDefaultAutopilot.autopilotContractSet}
.
+ >
+ ),
+ hidden: !isAutopilotEnabled || !showAdvanced,
+ validation:
+ isAutopilotEnabled && showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+
+ // hosts
+ allowRedundantIPs: {
+ type: 'boolean',
+ category: 'hosts',
+ title: 'Redundant IPs',
+ description: (
+ <>
+ Whether or not to allow forming contracts with multiple hosts in the
+ same IP subnet. The subnets used are /16 for IPv4, and /64 for IPv6.
+ >
+ ),
+ suggestion: advancedDefaultAutopilot.allowRedundantIPs,
+ suggestionTip: `Defaults to ${
+ advancedDefaultAutopilot.allowRedundantIPs ? 'on' : 'off'
+ }.`,
+ hidden: !isAutopilotEnabled || !showAdvanced,
+ validation: {},
+ },
+ maxDowntimeHours: {
+ type: 'number',
+ category: 'hosts',
+ title: 'Max downtime',
+ description: (
+ <>
+ The maximum amount of host downtime that autopilot will tolerate in
+ hours.
+ >
+ ),
+ units: 'hours',
+ suggestion: advancedDefaultAutopilot.maxDowntimeHours,
+ suggestionTip: `Defaults to ${advancedDefaultAutopilot.maxDowntimeHours
+ .toNumber()
+ .toLocaleString()} which is ${toFixedMax(
+ new BigNumber(
+ hoursInDays(advancedDefaultAutopilot.maxDowntimeHours.toNumber())
+ ),
+ 1
+ )} days.`,
+ hidden: !isAutopilotEnabled || !showAdvanced,
+ validation:
+ isAutopilotEnabled && showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+
+ // wallet
+ defragThreshold: {
+ type: 'number',
+ category: 'wallet',
+ title: 'Defrag threshold',
+ description: (
+ <>The threshold after which autopilot will defrag wallet outputs.>
+ ),
+ units: 'outputs',
+ suggestion: advancedDefaultAutopilot.defragThreshold,
+ suggestionTip: 'Defaults to 1,000.',
+ hidden: !isAutopilotEnabled || !showAdvanced,
+ validation:
+ isAutopilotEnabled && showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+
+ // contract
+ defaultContractSet: {
+ category: 'contractset',
+ type: 'text',
+ title: 'Default contract set',
+ placeholder: advancedDefaultContractSet.defaultContractSet,
+ suggestion: advancedDefaultContractSet.defaultContractSet,
+ suggestionTip: (
+ <>
+ Autopilot users will typically want to keep this the same as the
+ autopilot contract set.
+ >
+ ),
+ description: (
+ <>The default contract set is where data is uploaded to by default.>
+ ),
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+ uploadPackingEnabled: {
+ category: 'uploadpacking',
+ type: 'boolean',
+ title: 'Upload packing',
+ description: (
+ <>
+ Data on the Sia network is stored in 40MiB sectors so by default
+ uploaded files are divided and padded into these 40MiB peices. This
+ means that storage is wasted on padding but more importantly files
+ smaller than 40MiB still use 40MiB of space. Upload packing avoids
+ this waste by buffering files and packing them together before they
+ are uploaded to the network. This trades some performance for storage
+ efficiency. It is also important to note that because buffered files
+ are temporarily stored on disk they must be considered when backing up
+ your renterd data.
+ >
+ ),
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+
+ // gouging
+ maxStoragePriceTBMonth: {
+ category: 'gouging',
+ type: 'siacoin',
+ title: 'Max storage price',
+ description: <>The max allowed price to store 1 TB per month.>,
+ units: 'SC/TB/month',
+ average: storageAverage,
+ averageTip: getAverageTip(
+ includeRedundancyMaxStoragePrice,
+ redundancyMultiplier
+ ),
+ after: function After({ form, fields }) {
+ return (
+
+
+
+
+ Including {redundancyMultiplier.toFixed(1)}x redundancy
+
+
+
+
+ )
+ },
+ decimalsLimitSc: scDecimalPlaces,
+ validation: {
+ required: 'required',
+ },
+ },
+ maxUploadPriceTB: {
+ category: 'gouging',
+ type: 'siacoin',
+ title: 'Max upload price',
+ description: <>The max allowed price to upload 1 TB.>,
+ units: 'SC/TB/month',
+ average: uploadAverage,
+ averageTip: getAverageTip(
+ includeRedundancyMaxUploadPrice,
+ redundancyMultiplier
+ ),
+ after: function After({ form, fields }) {
+ return (
+
+
+
+
+ Including {redundancyMultiplier.toFixed(1)}x redundancy
+
+
+
+
+ )
+ },
+ decimalsLimitSc: scDecimalPlaces,
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+ maxDownloadPriceTB: {
+ category: 'gouging',
+ type: 'siacoin',
+ title: 'Max download price',
+ description: <>The max allowed price to download 1 TB.>,
+ units: 'SC/TB/month',
+ average: downloadAverage,
+ averageTip: `Averages provided by Sia Central.`,
+ decimalsLimitSc: scDecimalPlaces,
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+ maxContractPrice: {
+ category: 'gouging',
+ type: 'siacoin',
+ title: 'Max contract price',
+ description: <>The max allowed price to form a contract.>,
+ average: contractAverage,
+ decimalsLimitSc: scDecimalPlaces,
+ tipsDecimalsLimitSc: 3,
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+ maxRpcPriceMillion: {
+ category: 'gouging',
+ type: 'siacoin',
+ title: 'Max RPC price',
+ description: (
+ <>The max allowed base price for RPCs in siacoins per million calls.>
+ ),
+ units: 'SC/million',
+ decimalsLimitSc: scDecimalPlaces,
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+ minMaxCollateral: {
+ category: 'gouging',
+ type: 'siacoin',
+ title: 'Min max collateral',
+ description: (
+ <>The min value for max collateral in the host's price settings.>
+ ),
+ decimalsLimitSc: scDecimalPlaces,
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ }
+ : {},
+ },
+ hostBlockHeightLeeway: {
+ category: 'gouging',
+ type: 'number',
+ title: 'Block height leeway',
+ description: (
+ <>
+ The amount of blocks of leeway given to the host block height in the
+ host's price table.
+ >
+ ),
+ units: 'blocks',
+ decimalsLimit: 0,
+ suggestion: advancedDefaultGouging.hostBlockHeightLeeway,
+ suggestionTip: 'The recommended value is 6 blocks.',
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ validate: {
+ min: (value) =>
+ new BigNumber(value as BigNumber).gte(3) ||
+ 'must be at least 3 blocks',
+ },
+ }
+ : {},
+ },
+ minPriceTableValidityMinutes: {
+ category: 'gouging',
+ type: 'number',
+ title: 'Min price table validity',
+ units: 'minutes',
+ description: (
+ <>The min accepted value for `Validity` in the host's price settings.>
+ ),
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ validate: {
+ min: (value) =>
+ new BigNumber(value as BigNumber).gte(secondsInMinutes(10)) ||
+ 'must be at least 10 seconds',
+ },
+ }
+ : {},
+ },
+ minAccountExpiryDays: {
+ category: 'gouging',
+ type: 'number',
+ title: 'Min account expiry',
+ units: 'days',
+ description: (
+ <>
+ The min accepted value for `AccountExpiry` in the host's price
+ settings.
+ >
+ ),
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ validate: {
+ min: (value) =>
+ new BigNumber(value as BigNumber).gte(hoursInDays(1)) ||
+ 'must be at least 1 hour',
+ },
+ }
+ : {},
+ },
+ minMaxEphemeralAccountBalance: {
+ category: 'gouging',
+ type: 'siacoin',
+ title: 'Min max ephemeral account balance',
+ description: (
+ <>
+ The min accepted value for `MaxEphemeralAccountBalance` in the host's
+ price settings.
+ >
+ ),
+ decimalsLimitSc: scDecimalPlaces,
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ validate: {
+ min: (value) =>
+ new BigNumber(value as BigNumber).gte(1) ||
+ 'must be at least 1 SC',
+ },
+ }
+ : {},
+ },
+
+ // Redundancy
+ minShards: {
+ type: 'number',
+ category: 'redundancy',
+ title: 'Min shards',
+ description: <>The min amount of shards needed to reconstruct a slab.>,
+ units: 'shards',
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ validate: {
+ min: (value) =>
+ new BigNumber(value as BigNumber).gt(0) ||
+ 'must be greater than 0',
+ },
+ }
+ : {},
+ trigger: ['totalShards'],
+ },
+ totalShards: {
+ type: 'number',
+ category: 'redundancy',
+ title: 'Total shards',
+ description: <>The total amount of shards for each slab.>,
+ units: 'shards',
+ hidden: !showAdvanced,
+ validation: showAdvanced
+ ? {
+ required: 'required',
+ validate: {
+ gteMinShards: (value, values) =>
+ new BigNumber(value as BigNumber).gte(values.minShards) ||
+ 'must be at least equal to min shards',
+ max: (value) =>
+ new BigNumber(value as BigNumber).lt(256) ||
+ 'must be less than 256',
+ },
+ }
+ : {},
+ },
+
+ // hidden fields used by other config options
+ includeRedundancyMaxStoragePrice: {
+ type: 'boolean',
+ title: 'Include redundancy',
+ validation: {},
+ },
+ includeRedundancyMaxUploadPrice: {
+ type: 'boolean',
+ title: 'Include redundancy',
+ validation: {},
+ },
+ }
+}
+
+function getAverageTip(
+ includeRedundancy: boolean,
+ redundancyMultiplier: BigNumber
+) {
+ if (includeRedundancy) {
+ return `The average price is adjusted for ${redundancyMultiplier.toFixed(
+ 1
+ )}x redundancy. Averages provided by Sia Central.`
+ }
+ return `The average price is not adjusted for redundancy. Averages provided by Sia Central.`
+}
+
+function getRedundancyTip(
+ includeRedundancy: boolean,
+ redundancyMultiplier: BigNumber
+) {
+ if (includeRedundancy) {
+ return (
+
+
+ Specified max price includes the cost of{' '}
+ {redundancyMultiplier.toFixed(1)}x redundancy.
+
+
+ Redundancy is calculated from the ratio of data shards:{' '}
+ min shards / total shards
.
+
+
+ )
+ }
+ return `Specified max price does not include redundancy.`
+}
diff --git a/apps/renterd/contexts/config/index.tsx b/apps/renterd/contexts/config/index.tsx
new file mode 100644
index 000000000..79a3384c7
--- /dev/null
+++ b/apps/renterd/contexts/config/index.tsx
@@ -0,0 +1,542 @@
+import React, { createContext, useContext } from 'react'
+import {
+ triggerSuccessToast,
+ triggerErrorToast,
+ useOnInvalid,
+ monthsToBlocks,
+ TBToBytes,
+} from '@siafoundation/design-system'
+import BigNumber from 'bignumber.js'
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import {
+ AutopilotConfig,
+ autopilotHostsKey,
+ useAutopilotConfig,
+ useAutopilotConfigUpdate,
+ ContractSetSettings,
+ GougingSettings,
+ RedundancySettings,
+ UploadPackingSettings,
+ useSettingUpdate,
+} from '@siafoundation/react-renterd'
+import { toScale, toSiacoins } from '@siafoundation/sia-js'
+import { getFields } from './fields'
+import { SettingsData, defaultValues } from './types'
+import {
+ getRedundancyMultiplier,
+ getRedundancyMultiplierIfIncluded,
+ transformDown,
+ transformUpAutopilot,
+ transformUpConfigApp,
+ transformUpContractSet,
+ transformUpGouging,
+ transformUpRedundancy,
+ transformUpUploadPacking,
+} from './transform'
+import { useForm } from 'react-hook-form'
+import { useSyncContractSet } from './useSyncContractSet'
+import { delay, useMutate } from '@siafoundation/react-core'
+import { useContractSetSettings } from '../../hooks/useContractSetSettings'
+import {
+ ConfigDisplayOptions,
+ configDisplayOptionsKey,
+ useConfigDisplayOptions,
+} from '../../hooks/useConfigDisplayOptions'
+import { useGougingSettings } from '../../hooks/useGougingSettings'
+import { useRedundancySettings } from '../../hooks/useRedundancySettings'
+import { useUploadPackingSettings } from '../../hooks/useUploadPackingSettings'
+import { useSiaCentralHostsNetworkAverages } from '@siafoundation/react-sia-central'
+import useLocalStorageState from 'use-local-storage-state'
+import { useApp } from '../app'
+
+export function useConfigMain() {
+ const app = useApp()
+ const isAutopilotEnabled = app.autopilot.status === 'on'
+ // settings that 404 when empty
+ const autopilot = useAutopilotConfig({
+ disabled: !isAutopilotEnabled,
+ standalone: 'configFormAutopilot',
+ config: {
+ swr: {
+ errorRetryCount: 0,
+ },
+ },
+ })
+ const contractSet = useContractSetSettings({
+ standalone: 'configFormContractSet',
+ config: {
+ swr: {
+ errorRetryCount: 0,
+ },
+ },
+ })
+ const configApp = useConfigDisplayOptions({
+ standalone: 'configFormConfigApp',
+ config: {
+ swr: {
+ errorRetryCount: 0,
+ },
+ },
+ })
+ // settings with initial defaults
+ const gouging = useGougingSettings({
+ standalone: 'configFormGouging',
+ })
+ const redundancy = useRedundancySettings({
+ standalone: 'configFormRedundancy',
+ })
+ const uploadPacking = useUploadPackingSettings({
+ standalone: 'configFormUploadPacking',
+ })
+
+ const settingUpdate = useSettingUpdate()
+
+ const averages = useSiaCentralHostsNetworkAverages({
+ config: {
+ swr: {
+ revalidateOnFocus: false,
+ },
+ },
+ })
+ const [showAdvanced, setShowAdvanced] = useLocalStorageState(
+ 'v0/config/showAdvanced',
+ {
+ defaultValue: false,
+ }
+ )
+ const {
+ shouldSyncDefaultContractSet,
+ setShouldSyncDefaultContractSet,
+ syncDefaultContractSet,
+ } = useSyncContractSet()
+ const autopilotUpdate = useAutopilotConfigUpdate()
+
+ const form = useForm({
+ mode: 'all',
+ defaultValues,
+ })
+
+ const resetFormData = useCallback(
+ (
+ autopilotData: AutopilotConfig | undefined,
+ contractSetData: ContractSetSettings | undefined,
+ uploadPackingData: UploadPackingSettings,
+ gougingData: GougingSettings,
+ redundancyData: RedundancySettings,
+ configAppData: ConfigDisplayOptions | undefined
+ ) => {
+ const settingsData = transformDown(
+ autopilotData,
+ contractSetData,
+ uploadPackingData,
+ gougingData,
+ redundancyData,
+ configAppData
+ )
+ form.reset(settingsData)
+ return settingsData
+ },
+ [form]
+ )
+
+ const didDataRevalidate = useMemo(
+ () => [
+ autopilot.data,
+ autopilot.error,
+ contractSet.data,
+ contractSet.error,
+ uploadPacking.data,
+ gouging.data,
+ redundancy.data,
+ configApp.data,
+ configApp.error,
+ ],
+ [
+ autopilot.data,
+ autopilot.error,
+ contractSet.data,
+ contractSet.error,
+ uploadPacking.data,
+ gouging.data,
+ redundancy.data,
+ configApp.data,
+ configApp.error,
+ ]
+ )
+
+ const resetFormDataIfAllDataFetched = useCallback((): SettingsData | null => {
+ if (
+ (!isAutopilotEnabled || autopilot.data || autopilot.error) &&
+ gouging.data &&
+ redundancy.data &&
+ uploadPacking.data &&
+ (contractSet.data || contractSet.error) &&
+ (configApp.data || configApp.error)
+ ) {
+ return resetFormData(
+ autopilot.data,
+ contractSet.data,
+ uploadPacking.data,
+ gouging.data,
+ redundancy.data,
+ configApp.data
+ )
+ }
+ return null
+ }, [
+ isAutopilotEnabled,
+ resetFormData,
+ autopilot.data,
+ autopilot.error,
+ contractSet.data,
+ contractSet.error,
+ uploadPacking.data,
+ gouging.data,
+ redundancy.data,
+ configApp.data,
+ configApp.error,
+ ])
+
+ // init - when new config is fetched, set the form
+ const [hasInit, setHasInit] = useState(false)
+ useEffect(() => {
+ if (app.autopilot.status === 'init') {
+ return
+ }
+ if (!hasInit) {
+ const didReset = resetFormDataIfAllDataFetched()
+ if (didReset) {
+ setHasInit(true)
+ }
+ }
+ }, [hasInit, app.autopilot.status, resetFormDataIfAllDataFetched])
+
+ const revalidateAndResetFormData = useCallback(async () => {
+ const autopilotData = isAutopilotEnabled
+ ? await autopilot.mutate()
+ : undefined
+ const contractSetData = await contractSet.mutate()
+ const gougingData = await gouging.mutate()
+ const redundancyData = await redundancy.mutate()
+ const uploadPackingData = await uploadPacking.mutate()
+ const configAppData = await configApp.mutate()
+ if (!gougingData || !redundancyData) {
+ triggerErrorToast('Error fetching settings.')
+ } else {
+ resetFormData(
+ autopilotData,
+ contractSetData,
+ uploadPackingData,
+ gougingData,
+ redundancyData,
+ configAppData
+ )
+ }
+ }, [
+ isAutopilotEnabled,
+ autopilot,
+ contractSet,
+ gouging,
+ uploadPacking,
+ redundancy,
+ configApp,
+ resetFormData,
+ ])
+
+ const minShards = form.watch('minShards')
+ const totalShards = form.watch('totalShards')
+ const includeRedundancyMaxStoragePrice = form.watch(
+ 'includeRedundancyMaxStoragePrice'
+ )
+ 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({
+ isAutopilotEnabled,
+ showAdvanced,
+ redundancyMultiplier,
+ includeRedundancyMaxStoragePrice,
+ includeRedundancyMaxUploadPrice,
+ storageAverage: toSiacoins(averages.data.settings.storage_price) // bytes/block
+ .times(monthsToBlocks(1)) // bytes/month
+ .times(TBToBytes(1)) // TB/month
+ .times(
+ getRedundancyMultiplierIfIncluded(
+ minShards,
+ totalShards,
+ includeRedundancyMaxStoragePrice
+ )
+ ), // redundancy
+ uploadAverage: toSiacoins(averages.data.settings.upload_price) // bytes
+ .times(TBToBytes(1)) // TB
+ .times(
+ getRedundancyMultiplierIfIncluded(
+ minShards,
+ totalShards,
+ includeRedundancyMaxUploadPrice
+ )
+ ), // redundancy
+ downloadAverage: toSiacoins(averages.data.settings.download_price) // bytes
+ .times(TBToBytes(1)), // TB
+ contractAverage: toSiacoins(averages.data.settings.contract_price),
+ })
+ }
+ return getFields({
+ isAutopilotEnabled,
+ showAdvanced,
+ redundancyMultiplier,
+ includeRedundancyMaxStoragePrice,
+ includeRedundancyMaxUploadPrice,
+ })
+ }, [
+ isAutopilotEnabled,
+ showAdvanced,
+ averages.data,
+ redundancyMultiplier,
+ minShards,
+ totalShards,
+ includeRedundancyMaxStoragePrice,
+ includeRedundancyMaxUploadPrice,
+ ])
+
+ const mutate = useMutate()
+ const onValid = useCallback(
+ async (values: typeof defaultValues) => {
+ if (!gouging.data || !redundancy.data) {
+ return
+ }
+ try {
+ const firstTimeSettingConfig = isAutopilotEnabled && !autopilot.data
+ const autopilotResponse = isAutopilotEnabled
+ ? await autopilotUpdate.put({
+ payload: transformUpAutopilot(values, 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),
+ })
+ if (autopilotResponse?.error) {
+ throw Error(autopilotResponse.error)
+ }
+ if (contractSetResponse.error) {
+ throw Error(contractSetResponse.error)
+ }
+ if (uploadPackingResponse.error) {
+ throw Error(uploadPackingResponse.error)
+ }
+ if (gougingResponse.error) {
+ throw Error(gougingResponse.error)
+ }
+ if (redundancyResponse.error) {
+ throw Error(redundancyResponse.error)
+ }
+ if (configAppResponse.error) {
+ throw Error(configAppResponse.error)
+ }
+
+ triggerSuccessToast('Configuration has been saved.')
+ if (isAutopilotEnabled) {
+ syncDefaultContractSet(values.autopilotContractSet)
+ }
+
+ // if autopilot is being configured for the first time,
+ // revalidate the empty hosts list.
+ if (firstTimeSettingConfig) {
+ const refreshHostsAfterDelay = async () => {
+ await delay(5_000)
+ mutate((key) => key.startsWith(autopilotHostsKey))
+ await delay(5_000)
+ mutate((key) => key.startsWith(autopilotHostsKey))
+ }
+ refreshHostsAfterDelay()
+ }
+
+ await revalidateAndResetFormData()
+ } catch (e) {
+ triggerErrorToast((e as Error).message)
+ console.log(e)
+ }
+ },
+ [
+ isAutopilotEnabled,
+ autopilot,
+ autopilotUpdate,
+ revalidateAndResetFormData,
+ syncDefaultContractSet,
+ mutate,
+ settingUpdate,
+ contractSet,
+ uploadPacking,
+ redundancy,
+ gouging,
+ configApp,
+ ]
+ )
+
+ const onInvalid = useOnInvalid(fields)
+
+ const onSubmit = useMemo(
+ () => form.handleSubmit(onValid, onInvalid),
+ [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(() => {
+ const currentFormValues = form.getValues()
+ const serverFormValues = resetFormDataIfAllDataFetched()
+ form.reset(serverFormValues)
+ for (const [key, value] of Object.entries(currentFormValues)) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ form.setValue(key as any, value, {
+ shouldDirty: true,
+ })
+ }
+ }, [form, resetFormDataIfAllDataFetched])
+
+ useEffect(() => {
+ if (form.formState.isSubmitting) {
+ return
+ }
+ resetWithUserChanges()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ form,
+ // if form mode is toggled reset
+ showAdvanced,
+ // if any of the settings are revalidated reset
+ didDataRevalidate,
+ ])
+
+ const changeCount = Object.entries(form.formState.dirtyFields).filter(
+ ([_, val]) => !!val
+ ).length
+
+ return {
+ onSubmit,
+ revalidateAndResetFormData,
+ form,
+ fields,
+ changeCount,
+ canEstimate,
+ estimatedSpendingPerMonth,
+ estimatedSpendingPerTB,
+ redundancyMultiplier,
+ storageTB,
+ shouldSyncDefaultContractSet,
+ setShouldSyncDefaultContractSet,
+ showAdvanced,
+ setShowAdvanced,
+ }
+}
+
+type State = ReturnType
+
+const ConfigContext = createContext({} as State)
+export const useConfig = () => useContext(ConfigContext)
+
+type Props = {
+ children: React.ReactNode
+}
+
+export function ConfigProvider({ children }: Props) {
+ const state = useConfigMain()
+ return (
+ {children}
+ )
+}
diff --git a/apps/renterd/contexts/config/transform.spec.ts b/apps/renterd/contexts/config/transform.spec.ts
new file mode 100644
index 000000000..942ee5c46
--- /dev/null
+++ b/apps/renterd/contexts/config/transform.spec.ts
@@ -0,0 +1,553 @@
+import BigNumber from 'bignumber.js'
+import {
+ transformDown,
+ transformUpAutopilot,
+ transformUpContractSet,
+ transformUpGouging,
+ transformUpRedundancy,
+ valuePerMonthToPerPeriod,
+ valuePerPeriodToPerMonth,
+} from './transform'
+import { SettingsData } from './types'
+import {
+ blocksToWeeks,
+ monthsToBlocks,
+ weeksToBlocks,
+} from '@siafoundation/design-system'
+import { toHastings } from '@siafoundation/sia-js'
+
+describe('tansforms', () => {
+ describe('down', () => {
+ it('default works', () => {
+ expect(
+ transformDown(
+ {
+ wallet: {
+ defragThreshold: 1000,
+ },
+ hosts: {
+ allowRedundantIPs: false,
+ maxDowntimeHours: 1440,
+ scoreOverrides: null,
+ },
+ contracts: {
+ set: 'autopilot',
+ amount: 51,
+ allowance: toHastings(500).toString(),
+ period: monthsToBlocks(1),
+ renewWindow: 2248,
+ download: 1099511627776,
+ upload: 1100000000000,
+ storage: 1000000000000,
+ },
+ },
+ { default: 'myset' },
+ { enabled: true },
+ {
+ hostBlockHeightLeeway: 4,
+ maxContractPrice: '20000000000000000000000000',
+ maxDownloadPrice: '1004310000000000000000000000',
+ maxRPCPrice: '99970619000000000000000000',
+ maxStoragePrice: '210531181019',
+ maxUploadPrice: '1000232323000000000000000000',
+ minAccountExpiry: 86400000000000,
+ minMaxCollateral: '10000000000000000000000000',
+ minMaxEphemeralAccountBalance: '1000000000000000000000000',
+ minPriceTableValidity: 300000000000,
+ },
+ {
+ minShards: 10,
+ totalShards: 30,
+ },
+ {
+ includeRedundancyMaxStoragePrice: false,
+ includeRedundancyMaxUploadPrice: false,
+ }
+ )
+ ).toEqual({
+ autopilotContractSet: 'autopilot',
+ allowanceMonth: new BigNumber('500'),
+ amountHosts: new BigNumber('51'),
+ periodWeeks: new BigNumber('4.285714285714286'),
+ renewWindowWeeks: new BigNumber('2.2301587301587302'),
+ downloadTBMonth: new BigNumber('1.1'),
+ uploadTBMonth: new BigNumber('1.1'),
+ storageTB: new BigNumber('1'),
+ allowRedundantIPs: false,
+ maxDowntimeHours: new BigNumber('1440'),
+ defragThreshold: new BigNumber('1000'),
+ defaultContractSet: 'myset',
+ uploadPackingEnabled: true,
+ hostBlockHeightLeeway: new BigNumber(4),
+ maxContractPrice: new BigNumber('20'),
+ maxDownloadPriceTB: new BigNumber('1004.31'),
+ maxRpcPriceMillion: new BigNumber('99970619'),
+ maxStoragePriceTBMonth: new BigNumber('909.494702'),
+ maxUploadPriceTB: new BigNumber('1000.232323'),
+ minAccountExpiryDays: new BigNumber(1),
+ minMaxCollateral: new BigNumber('10'),
+ minMaxEphemeralAccountBalance: new BigNumber('1'),
+ minPriceTableValidityMinutes: new BigNumber(5),
+ minShards: new BigNumber(10),
+ totalShards: new BigNumber(30),
+ includeRedundancyMaxStoragePrice: false,
+ includeRedundancyMaxUploadPrice: false,
+ } as SettingsData)
+ })
+
+ it('with include redundancy for storage and upload', () => {
+ expect(
+ transformDown(
+ {
+ wallet: {
+ defragThreshold: 1000,
+ },
+ hosts: {
+ allowRedundantIPs: false,
+ maxDowntimeHours: 1440,
+ scoreOverrides: null,
+ },
+ contracts: {
+ set: 'autopilot',
+ amount: 51,
+ allowance: '8408400000000000000000000000',
+ period: 6048,
+ renewWindow: 2248,
+ download: 1099511627776,
+ upload: 1100000000000,
+ storage: 1000000000000,
+ },
+ },
+ { default: 'myset' },
+ { enabled: true },
+ {
+ hostBlockHeightLeeway: 4,
+ maxContractPrice: '20000000000000000000000000',
+ maxDownloadPrice: '1004310000000000000000000000',
+ maxRPCPrice: '99970619000000000000000000',
+ maxStoragePrice: '210531181019',
+ maxUploadPrice: '1000232323000000000000000000',
+ minAccountExpiry: 86400000000000,
+ minMaxCollateral: '10000000000000000000000000',
+ minMaxEphemeralAccountBalance: '1000000000000000000000000',
+ minPriceTableValidity: 300000000000,
+ },
+ {
+ minShards: 10,
+ totalShards: 30,
+ },
+ {
+ includeRedundancyMaxStoragePrice: true,
+ includeRedundancyMaxUploadPrice: true,
+ }
+ )
+ ).toEqual({
+ autopilotContractSet: 'autopilot',
+ allowanceMonth: new BigNumber('6006'),
+ amountHosts: new BigNumber('51'),
+ periodWeeks: new BigNumber('6'),
+ renewWindowWeeks: new BigNumber('2.2301587301587302'),
+ downloadTBMonth: new BigNumber('0.79'),
+ uploadTBMonth: new BigNumber('0.79'),
+ storageTB: new BigNumber('1'),
+ allowRedundantIPs: false,
+ maxDowntimeHours: new BigNumber('1440'),
+ defragThreshold: new BigNumber('1000'),
+ defaultContractSet: 'myset',
+ uploadPackingEnabled: true,
+ hostBlockHeightLeeway: new BigNumber(4),
+ maxContractPrice: new BigNumber('20'),
+ maxDownloadPriceTB: new BigNumber('1004.31'),
+ maxRpcPriceMillion: new BigNumber('99970619'),
+ maxStoragePriceTBMonth: new BigNumber('2728.484106'),
+ maxUploadPriceTB: new BigNumber('3000.696969'),
+ minAccountExpiryDays: new BigNumber(1),
+ minMaxCollateral: new BigNumber('10'),
+ minMaxEphemeralAccountBalance: new BigNumber('1'),
+ minPriceTableValidityMinutes: new BigNumber(5),
+ minShards: new BigNumber(10),
+ totalShards: new BigNumber(30),
+ includeRedundancyMaxStoragePrice: true,
+ includeRedundancyMaxUploadPrice: true,
+ } as SettingsData)
+ })
+ })
+
+ describe('up', () => {
+ it('up autopilot', () => {
+ expect(
+ transformUpAutopilot(
+ {
+ autopilotContractSet: 'autopilot',
+ allowanceMonth: new BigNumber('6006'),
+ amountHosts: new BigNumber('51'),
+ periodWeeks: new BigNumber('6'),
+ renewWindowWeeks: new BigNumber('2.2301587301587302'),
+ downloadTBMonth: new BigNumber('0.785365448411428571428571428571'),
+ uploadTBMonth: new BigNumber('0.785714285714285714285714285714'),
+ storageTB: new BigNumber('1'),
+ allowRedundantIPs: false,
+ maxDowntimeHours: new BigNumber('1440'),
+ defragThreshold: new BigNumber('1000'),
+ },
+ undefined
+ )
+ ).toEqual({
+ wallet: {
+ defragThreshold: 1000,
+ },
+ hosts: {
+ allowRedundantIPs: false,
+ maxDowntimeHours: 1440,
+ scoreOverrides: null,
+ },
+ contracts: {
+ set: 'autopilot',
+ amount: 51,
+ allowance: '8408400000000000000000000000',
+ period: 6048,
+ renewWindow: 2248,
+ download: 1099511627776,
+ upload: 1100000000000,
+ storage: 1000000000000,
+ },
+ })
+ })
+
+ it('up autopilot accepts unknown values', () => {
+ expect(
+ transformUpAutopilot(
+ {
+ autopilotContractSet: 'autopilot',
+ allowanceMonth: new BigNumber('6006'),
+ amountHosts: new BigNumber('51'),
+ periodWeeks: new BigNumber('6'),
+ renewWindowWeeks: new BigNumber('2.2301587301587302'),
+ downloadTBMonth: new BigNumber('0.785365448411428571428571428571'),
+ uploadTBMonth: new BigNumber('0.785714285714285714285714285714'),
+ storageTB: new BigNumber('1'),
+ allowRedundantIPs: false,
+ maxDowntimeHours: new BigNumber('1440'),
+ defragThreshold: new BigNumber('1000'),
+ },
+ {
+ foobar1: 'value',
+ wallet: {
+ foobar: 'value',
+ },
+ contracts: {
+ foobar: 'value',
+ period: 7777,
+ },
+ hosts: {
+ foobar: 'value',
+ },
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any
+ )
+ ).toEqual({
+ foobar1: 'value',
+ wallet: {
+ foobar: 'value',
+ defragThreshold: 1000,
+ },
+ hosts: {
+ foobar: 'value',
+ allowRedundantIPs: false,
+ maxDowntimeHours: 1440,
+ scoreOverrides: null,
+ },
+ contracts: {
+ foobar: 'value',
+ set: 'autopilot',
+ amount: 51,
+ allowance: '8408400000000000000000000000',
+ period: 6048,
+ renewWindow: 2248,
+ download: 1099511627776,
+ upload: 1100000000000,
+ storage: 1000000000000,
+ },
+ })
+ })
+
+ it('up contractset', () => {
+ expect(
+ transformUpContractSet(
+ {
+ defaultContractSet: 'myset',
+ },
+ {
+ default: '77777777777',
+ foobar: 'value',
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any
+ )
+ ).toEqual({
+ default: 'myset',
+ foobar: 'value',
+ })
+ })
+
+ it('up gouging', () => {
+ expect(
+ transformUpGouging(
+ {
+ autopilotContractSet: 'autopilot',
+ allowanceMonth: new BigNumber('6006'),
+ amountHosts: new BigNumber('51'),
+ periodWeeks: new BigNumber('6'),
+ renewWindowWeeks: new BigNumber('2.2301587301587302'),
+ downloadTBMonth: new BigNumber('0.785365448411428571428571428571'),
+ uploadTBMonth: new BigNumber('0.785714285714285714285714285714'),
+ storageTB: new BigNumber('1'),
+ allowRedundantIPs: false,
+ maxDowntimeHours: new BigNumber('1440'),
+ defragThreshold: new BigNumber('1000'),
+ defaultContractSet: 'myset',
+ uploadPackingEnabled: false,
+ hostBlockHeightLeeway: new BigNumber(4),
+ maxContractPrice: new BigNumber('20'),
+ maxDownloadPriceTB: new BigNumber('1004.31'),
+ maxRpcPriceMillion: new BigNumber('99970619'),
+ maxStoragePriceTBMonth: new BigNumber('909.494702'),
+ maxUploadPriceTB: new BigNumber('1000.232323'),
+ minAccountExpiryDays: new BigNumber(1),
+ minMaxCollateral: new BigNumber('10'),
+ minMaxEphemeralAccountBalance: new BigNumber('1'),
+ minPriceTableValidityMinutes: new BigNumber(5),
+ minShards: new BigNumber(10),
+ totalShards: new BigNumber(30),
+ includeRedundancyMaxStoragePrice: false,
+ includeRedundancyMaxUploadPrice: false,
+ },
+ {
+ maxStoragePrice: '77777777777',
+ foobar: 'value',
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any
+ )
+ ).toEqual({
+ foobar: 'value',
+ hostBlockHeightLeeway: 4,
+ maxContractPrice: '20000000000000000000000000',
+ maxDownloadPrice: '1004310000000000000000000000',
+ maxRPCPrice: '99970619000000000000000000',
+ maxStoragePrice: '210531181019',
+ maxUploadPrice: '1000232323000000000000000000',
+ minAccountExpiry: 86400000000000,
+ minMaxCollateral: '10000000000000000000000000',
+ minMaxEphemeralAccountBalance: '1000000000000000000000000',
+ minPriceTableValidity: 300000000000,
+ })
+ })
+
+ it('up gouging with include redundancy for storage', () => {
+ expect(
+ transformUpGouging(
+ {
+ autopilotContractSet: 'autopilot',
+ allowanceMonth: new BigNumber('6006'),
+ amountHosts: new BigNumber('51'),
+ periodWeeks: new BigNumber('6'),
+ renewWindowWeeks: new BigNumber('2.2301587301587302'),
+ downloadTBMonth: new BigNumber('0.785365448411428571428571428571'),
+ uploadTBMonth: new BigNumber('0.785714285714285714285714285714'),
+ storageTB: new BigNumber('1'),
+ allowRedundantIPs: false,
+ maxDowntimeHours: new BigNumber('1440'),
+ defragThreshold: new BigNumber('1000'),
+ defaultContractSet: 'myset',
+ uploadPackingEnabled: false,
+ hostBlockHeightLeeway: new BigNumber(4),
+ maxContractPrice: new BigNumber('20'),
+ maxDownloadPriceTB: new BigNumber('1004.31'),
+ maxRpcPriceMillion: new BigNumber('99970619'),
+ maxStoragePriceTBMonth: new BigNumber('909.494702'),
+ maxUploadPriceTB: new BigNumber('1000.232323'),
+ minAccountExpiryDays: new BigNumber(1),
+ minMaxCollateral: new BigNumber('10'),
+ minMaxEphemeralAccountBalance: new BigNumber('1'),
+ minPriceTableValidityMinutes: new BigNumber(5),
+ minShards: new BigNumber(10),
+ totalShards: new BigNumber(30),
+ includeRedundancyMaxStoragePrice: true,
+ includeRedundancyMaxUploadPrice: false,
+ },
+ {
+ maxStoragePrice: '77777777777',
+ foobar: 'value',
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any
+ )
+ ).toEqual({
+ foobar: 'value',
+ hostBlockHeightLeeway: 4,
+ maxContractPrice: '20000000000000000000000000',
+ maxDownloadPrice: '1004310000000000000000000000',
+ maxRPCPrice: '99970619000000000000000000',
+ maxStoragePrice: '70177060340',
+ maxUploadPrice: '1000232323000000000000000000',
+ minAccountExpiry: 86400000000000,
+ minMaxCollateral: '10000000000000000000000000',
+ minMaxEphemeralAccountBalance: '1000000000000000000000000',
+ minPriceTableValidity: 300000000000,
+ })
+ })
+
+ it('up redundancy', () => {
+ expect(
+ transformUpRedundancy(
+ {
+ minShards: new BigNumber(10),
+ totalShards: new BigNumber(30),
+ },
+ {
+ minShards: 77,
+ foobar: 'value',
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any
+ )
+ ).toEqual({
+ foobar: 'value',
+ minShards: 10,
+ totalShards: 30,
+ })
+ })
+ })
+
+ describe('up down', () => {
+ it('converts ap download up down', () => {
+ const {
+ autopilot,
+ contractSet,
+ uploadPacking,
+ gouging,
+ redundancy,
+ display,
+ } = buildAllResponses()
+ let settings = transformDown(
+ {
+ ...autopilot,
+ contracts: {
+ ...autopilot.contracts,
+ download: 91085125718831,
+ period: 4244,
+ },
+ },
+ contractSet,
+ uploadPacking,
+ gouging,
+ redundancy,
+ display
+ )
+ expect(settings.downloadTBMonth).toEqual(new BigNumber('92.72'))
+ // a little different due to rounding
+ expect(
+ transformUpAutopilot(settings, autopilot).contracts.download
+ ).toEqual(91088814814815)
+
+ settings = transformDown(
+ {
+ ...autopilot,
+ contracts: {
+ ...autopilot.contracts,
+ download: 91088814814815,
+ period: 4244,
+ },
+ },
+ contractSet,
+ uploadPacking,
+ gouging,
+ redundancy,
+ display
+ )
+ expect(settings.downloadTBMonth).toEqual(new BigNumber('92.72'))
+ // using the rounded value results in same value
+ expect(
+ transformUpAutopilot(settings, autopilot).contracts.download
+ ).toEqual(91088814814815)
+ })
+ })
+
+ it('ap download', () => {
+ const valuePerPeriod = new BigNumber(91085125718831)
+ const periodBlocks = new BigNumber(4244)
+ const valuePerMonth = valuePerPeriodToPerMonth(
+ valuePerPeriod,
+ periodBlocks.toNumber()
+ )
+ expect(valuePerMonth).toEqual(
+ new BigNumber('92716244841034.38265786993402450518378887952')
+ )
+ const periodWeeks = new BigNumber(blocksToWeeks(periodBlocks.toNumber()))
+ expect(
+ valuePerMonthToPerPeriod(valuePerMonth, periodWeeks).toFixed(0)
+ ).toEqual(valuePerPeriod.toString())
+ })
+
+ it('month -> period', () => {
+ const valuePerMonth = new BigNumber(87908469486735)
+ const periodWeeks = new BigNumber(30).div(7)
+ expect(valuePerMonthToPerPeriod(valuePerMonth, periodWeeks)).toEqual(
+ valuePerMonth
+ )
+ })
+ it('period <- month', () => {
+ const valuePerMonth = new BigNumber(30)
+ const periodWeeks = new BigNumber(30).div(7)
+ const valuePerPeriod = valuePerMonthToPerPeriod(valuePerMonth, periodWeeks)
+
+ const periodBlocks = weeksToBlocks(periodWeeks.toNumber())
+ expect(
+ valuePerPeriodToPerMonth(valuePerPeriod, periodBlocks).toFixed(0)
+ ).toEqual('30')
+ })
+})
+
+function buildAllResponses() {
+ return {
+ autopilot: {
+ wallet: {
+ defragThreshold: 1000,
+ },
+ hosts: {
+ allowRedundantIPs: false,
+ maxDowntimeHours: 1440,
+ scoreOverrides: null,
+ },
+ contracts: {
+ set: 'autopilot',
+ amount: 51,
+ allowance: toHastings(500).toString(),
+ period: monthsToBlocks(1),
+ renewWindow: 2248,
+ download: 1099511627776,
+ upload: 1100000000000,
+ storage: 1000000000000,
+ },
+ },
+ contractSet: { default: 'myset' },
+ uploadPacking: { enabled: true },
+ gouging: {
+ hostBlockHeightLeeway: 4,
+ maxContractPrice: '20000000000000000000000000',
+ maxDownloadPrice: '1004310000000000000000000000',
+ maxRPCPrice: '99970619000000000000000000',
+ maxStoragePrice: '210531181019',
+ maxUploadPrice: '1000232323000000000000000000',
+ minAccountExpiry: 86400000000000,
+ minMaxCollateral: '10000000000000000000000000',
+ minMaxEphemeralAccountBalance: '1000000000000000000000000',
+ minPriceTableValidity: 300000000000,
+ },
+ redundancy: {
+ minShards: 10,
+ totalShards: 30,
+ },
+ display: {
+ includeRedundancyMaxStoragePrice: false,
+ includeRedundancyMaxUploadPrice: false,
+ },
+ }
+}
diff --git a/apps/renterd/components/Config/transform.ts b/apps/renterd/contexts/config/transform.ts
similarity index 50%
rename from apps/renterd/components/Config/transform.ts
rename to apps/renterd/contexts/config/transform.ts
index 12d29353a..95eab430a 100644
--- a/apps/renterd/components/Config/transform.ts
+++ b/apps/renterd/contexts/config/transform.ts
@@ -1,47 +1,117 @@
import {
+ blocksToWeeks,
+ bytesToTB,
+ weeksToBlocks,
daysInNanoseconds,
minutesInNanoseconds,
monthsToBlocks,
nanosecondsInDays,
nanosecondsInMinutes,
TBToBytes,
+ toFixedMax,
} from '@siafoundation/design-system'
import {
+ AutopilotConfig,
ContractSetSettings,
GougingSettings,
RedundancySettings,
UploadPackingSettings,
} from '@siafoundation/react-renterd'
import { toHastings, toSiacoins } from '@siafoundation/sia-js'
-import { ConfigDisplayOptions } from '../../hooks/useConfigDisplayOptions'
import BigNumber from 'bignumber.js'
import {
+ AutopilotData,
+ scDecimalPlaces,
+ SettingsData,
+ advancedDefaultAutopilot,
ConfigAppData,
ContractSetData,
defaultConfigApp,
defaultContractSet,
GougingData,
RedundancyData,
- scDecimalPlaces,
- SettingsData,
UploadPackingData,
-} from './fields'
+ defaultAutopilot,
+ advancedDefaultContractSet,
+} from './types'
+import { ConfigDisplayOptions } from '../../hooks/useConfigDisplayOptions'
+
+const filterUndefinedKeys = (obj: Record) => {
+ return Object.fromEntries(
+ Object.entries(obj).filter(
+ ([key, value]) => value !== undefined && value !== ''
+ )
+ )
+}
// up
+export function transformUpAutopilot(
+ values: AutopilotData,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ existingValues: AutopilotConfig | undefined
+): AutopilotConfig {
+ // merge suggestions with values, if advanced values are required they will
+ // be added before this function is called and will override suggestions
+ const v: AutopilotData = {
+ ...advancedDefaultAutopilot,
+ ...filterUndefinedKeys(values),
+ }
+
+ return {
+ ...existingValues,
+ contracts: {
+ ...existingValues?.contracts,
+ set: v.autopilotContractSet,
+ amount: Math.round(v.amountHosts.toNumber()),
+ allowance: toHastings(
+ valuePerMonthToPerPeriod(v.allowanceMonth, v.periodWeeks)
+ ).toString(),
+ period: Math.round(weeksToBlocks(v.periodWeeks.toNumber())),
+ renewWindow: Math.round(weeksToBlocks(v.renewWindowWeeks.toNumber())),
+ download: Number(
+ valuePerMonthToPerPeriod(
+ TBToBytes(v.downloadTBMonth),
+ v.periodWeeks
+ ).toFixed(0)
+ ),
+ upload: Number(
+ valuePerMonthToPerPeriod(
+ TBToBytes(v.uploadTBMonth),
+ v.periodWeeks
+ ).toFixed(0)
+ ),
+ storage: TBToBytes(v.storageTB).toNumber(),
+ },
+ hosts: {
+ ...existingValues?.hosts,
+ maxDowntimeHours: v.maxDowntimeHours.toNumber(),
+ allowRedundantIPs: v.allowRedundantIPs,
+ scoreOverrides: existingValues?.hosts.scoreOverrides || null,
+ },
+ wallet: {
+ ...existingValues?.wallet,
+ defragThreshold: v.defragThreshold.toNumber(),
+ },
+ }
+}
export function transformUpContractSet(
values: ContractSetData,
- existingValues: Record
+ existingValues: ContractSetSettings | undefined
): ContractSetSettings {
+ const _default =
+ values.defaultContractSet ||
+ (existingValues?.default as string) ||
+ advancedDefaultContractSet.defaultContractSet
return {
...existingValues,
- default: values.contractSet,
+ default: _default,
}
}
export function transformUpUploadPacking(
values: UploadPackingData,
- existingValues: Record
+ existingValues: UploadPackingSettings
): UploadPackingSettings {
return {
...existingValues,
@@ -51,13 +121,15 @@ export function transformUpUploadPacking(
export function transformUpGouging(
values: SettingsData,
- existingValues: Record
+ existingValues: GougingSettings
): GougingSettings {
return {
...existingValues,
- maxRPCPrice: toHastings(values.maxRpcPrice.div(1_000_000)).toString(),
+ maxRPCPrice: toHastings(
+ values.maxRpcPriceMillion.div(1_000_000)
+ ).toString(),
maxStoragePrice: toHastings(
- values.maxStoragePrice // TB/month
+ values.maxStoragePriceTBMonth // TB/month
.div(monthsToBlocks(1)) // TB/block
.div(TBToBytes(1))
.div(
@@ -69,7 +141,7 @@ export function transformUpGouging(
) // bytes/block
).toString(),
maxUploadPrice: toHastings(
- values.maxUploadPrice.div(
+ values.maxUploadPriceTB.div(
getRedundancyMultiplierIfIncluded(
values.minShards,
values.totalShards,
@@ -77,15 +149,15 @@ export function transformUpGouging(
)
)
).toString(),
- maxDownloadPrice: toHastings(values.maxDownloadPrice).toString(),
+ maxDownloadPrice: toHastings(values.maxDownloadPriceTB).toString(),
maxContractPrice: toHastings(values.maxContractPrice).toString(),
minMaxCollateral: toHastings(values.minMaxCollateral).toString(),
hostBlockHeightLeeway: Math.round(values.hostBlockHeightLeeway.toNumber()),
minPriceTableValidity: Math.round(
- minutesInNanoseconds(values.minPriceTableValidity.toNumber())
+ minutesInNanoseconds(values.minPriceTableValidityMinutes.toNumber())
),
minAccountExpiry: Math.round(
- daysInNanoseconds(values.minAccountExpiry.toNumber())
+ daysInNanoseconds(values.minAccountExpiryDays.toNumber())
),
minMaxEphemeralAccountBalance: toHastings(
values.minMaxEphemeralAccountBalance
@@ -95,7 +167,7 @@ export function transformUpGouging(
export function transformUpRedundancy(
values: RedundancyData,
- existingValues: Record
+ existingValues: RedundancySettings
): RedundancySettings {
return {
...existingValues,
@@ -106,7 +178,7 @@ export function transformUpRedundancy(
export function transformUpConfigApp(
values: ConfigAppData,
- existingValues: Record
+ existingValues: Record | undefined
): ConfigDisplayOptions {
return {
...existingValues,
@@ -116,6 +188,63 @@ export function transformUpConfigApp(
}
// down
+export function transformDownAutopilot(
+ config?: AutopilotConfig
+): AutopilotData {
+ if (!config) {
+ return defaultAutopilot
+ }
+
+ const autopilotContractSet = config.contracts.set
+ const allowanceMonth = toSiacoins(
+ valuePerPeriodToPerMonth(
+ new BigNumber(config.contracts.allowance),
+ config.contracts.period
+ ),
+ scDecimalPlaces
+ )
+ const amountHosts = new BigNumber(config.contracts.amount)
+ const periodWeeks = new BigNumber(blocksToWeeks(config.contracts.period))
+ const renewWindowWeeks = new BigNumber(
+ blocksToWeeks(config.contracts.renewWindow)
+ )
+ const downloadTBMonth = new BigNumber(
+ toFixedMax(
+ valuePerPeriodToPerMonth(
+ bytesToTB(config.contracts.download),
+ config.contracts.period
+ ),
+ 2
+ )
+ )
+ const uploadTBMonth = new BigNumber(
+ toFixedMax(
+ valuePerPeriodToPerMonth(
+ bytesToTB(config.contracts.upload),
+ config.contracts.period
+ ),
+ 2
+ )
+ )
+ const storageTB = bytesToTB(new BigNumber(config.contracts.storage))
+
+ return {
+ // contracts
+ autopilotContractSet,
+ allowanceMonth,
+ amountHosts,
+ periodWeeks,
+ renewWindowWeeks,
+ downloadTBMonth,
+ uploadTBMonth,
+ storageTB,
+ // hosts
+ allowRedundantIPs: config.hosts.allowRedundantIPs,
+ maxDowntimeHours: new BigNumber(config.hosts.maxDowntimeHours),
+ // wallet
+ defragThreshold: new BigNumber(config.wallet.defragThreshold),
+ }
+}
export function transformDownContractSet(
c?: ContractSetSettings
@@ -124,7 +253,7 @@ export function transformDownContractSet(
return defaultContractSet
}
return {
- contractSet: c.default,
+ defaultContractSet: c.default,
}
}
@@ -154,7 +283,7 @@ export function transformDownGouging(
ca: ConfigAppData
): GougingData {
return {
- maxStoragePrice: toSiacoins(
+ maxStoragePriceTBMonth: toSiacoins(
new BigNumber(g.maxStoragePrice) // bytes/block
.times(monthsToBlocks(1)) // bytes/month
.times(TBToBytes(1)) // tb/month
@@ -167,7 +296,7 @@ export function transformDownGouging(
),
scDecimalPlaces
), // TB/month
- maxUploadPrice: toSiacoins(
+ maxUploadPriceTB: toSiacoins(
new BigNumber(g.maxUploadPrice).times(
getRedundancyMultiplierIfIncluded(
r.minShards,
@@ -177,15 +306,17 @@ export function transformDownGouging(
),
scDecimalPlaces
),
- maxDownloadPrice: toSiacoins(g.maxDownloadPrice, scDecimalPlaces),
+ maxDownloadPriceTB: toSiacoins(g.maxDownloadPrice, scDecimalPlaces),
maxContractPrice: toSiacoins(g.maxContractPrice, scDecimalPlaces),
- maxRpcPrice: toSiacoins(g.maxRPCPrice, scDecimalPlaces).times(1_000_000),
+ maxRpcPriceMillion: toSiacoins(g.maxRPCPrice, scDecimalPlaces).times(
+ 1_000_000
+ ),
minMaxCollateral: toSiacoins(g.minMaxCollateral, scDecimalPlaces),
hostBlockHeightLeeway: new BigNumber(g.hostBlockHeightLeeway),
- minPriceTableValidity: new BigNumber(
+ minPriceTableValidityMinutes: new BigNumber(
nanosecondsInMinutes(g.minPriceTableValidity)
),
- minAccountExpiry: new BigNumber(nanosecondsInDays(g.minAccountExpiry)),
+ minAccountExpiryDays: new BigNumber(nanosecondsInDays(g.minAccountExpiry)),
minMaxEphemeralAccountBalance: toSiacoins(
g.minMaxEphemeralAccountBalance,
scDecimalPlaces
@@ -201,6 +332,7 @@ export function transformDownRedundancy(r: RedundancySettings): RedundancyData {
}
export function transformDown(
+ a: AutopilotConfig | undefined,
c: ContractSetSettings | undefined,
u: UploadPackingSettings,
g: GougingSettings,
@@ -210,6 +342,8 @@ export function transformDown(
const configApp = transformDownConfigApp(ca)
const redundancy = transformDownRedundancy(r)
return {
+ // autopilot
+ ...transformDownAutopilot(a),
// contractset
...transformDownContractSet(c),
// uploadpacking
@@ -248,3 +382,35 @@ export function getRedundancyMultiplierIfIncluded(
const redundancyMult = getRedundancyMultiplier(minShards, totalShards)
return includeRedundancy ? redundancyMult : new BigNumber(1)
}
+
+export function storagePricePerMonthToPerBlock(value: BigNumber) {
+ return value // TB/month
+ .div(monthsToBlocks(1)) // TB/block
+ .div(TBToBytes(1)) // bytes/block
+}
+export function storagePricePerMonthToPerBlockWithRedundancy(
+ value: BigNumber,
+ minShards: BigNumber,
+ totalShards: BigNumber,
+ includeRedundancy: boolean
+) {
+ return storagePricePerMonthToPerBlock(value).div(
+ getRedundancyMultiplierIfIncluded(minShards, totalShards, includeRedundancy)
+ )
+}
+
+export function valuePerMonthToPerPeriod(
+ valuePerMonth: BigNumber,
+ periodWeeks: BigNumber
+) {
+ const periodBlocks = weeksToBlocks(periodWeeks.toNumber())
+ return valuePerMonth.times(periodBlocks).div(monthsToBlocks(1))
+}
+
+export function valuePerPeriodToPerMonth(
+ valuePerPeriod: BigNumber,
+ periodBlocks: number
+) {
+ const valuePerBlock = valuePerPeriod.div(periodBlocks)
+ return valuePerBlock.times(monthsToBlocks(1))
+}
diff --git a/apps/renterd/contexts/config/types.ts b/apps/renterd/contexts/config/types.ts
new file mode 100644
index 000000000..148316812
--- /dev/null
+++ b/apps/renterd/contexts/config/types.ts
@@ -0,0 +1,110 @@
+import BigNumber from 'bignumber.js'
+
+export const scDecimalPlaces = 6
+
+// form defaults
+export const defaultAutopilot = {
+ // contracts
+ autopilotContractSet: '',
+ amountHosts: undefined as BigNumber | undefined,
+ allowanceMonth: undefined as BigNumber | undefined,
+ periodWeeks: undefined as BigNumber | undefined,
+ renewWindowWeeks: undefined as BigNumber | undefined,
+ downloadTBMonth: undefined as BigNumber | undefined,
+ uploadTBMonth: undefined as BigNumber | undefined,
+ storageTB: undefined as BigNumber | undefined,
+ // hosts
+ allowRedundantIPs: false,
+ maxDowntimeHours: undefined as BigNumber | undefined,
+ // wallet
+ defragThreshold: undefined as BigNumber | undefined,
+}
+
+export const defaultContractSet = {
+ defaultContractSet: '',
+}
+
+export const defaultUploadPacking = {
+ uploadPackingEnabled: true,
+}
+
+export const defaultConfigApp = {
+ includeRedundancyMaxStoragePrice: true,
+ includeRedundancyMaxUploadPrice: true,
+}
+
+export const defaultGouging = {
+ maxRpcPriceMillion: undefined as BigNumber | undefined,
+ maxStoragePriceTBMonth: undefined as BigNumber | undefined,
+ maxContractPrice: undefined as BigNumber | undefined,
+ maxDownloadPriceTB: undefined as BigNumber | undefined,
+ maxUploadPriceTB: undefined as BigNumber | undefined,
+ minMaxCollateral: undefined as BigNumber | undefined,
+ hostBlockHeightLeeway: undefined as BigNumber | undefined,
+ minPriceTableValidityMinutes: undefined as BigNumber | undefined,
+ minAccountExpiryDays: undefined as BigNumber | undefined,
+ minMaxEphemeralAccountBalance: undefined as BigNumber | undefined,
+}
+
+export const defaultRedundancy = {
+ minShards: undefined as BigNumber | undefined,
+ totalShards: undefined as BigNumber | undefined,
+}
+
+export const defaultValues = {
+ // autopilot
+ ...defaultAutopilot,
+ // contract set
+ ...defaultContractSet,
+ // upload packing
+ ...defaultUploadPacking,
+ // gouging
+ ...defaultGouging,
+ // redundancy
+ ...defaultRedundancy,
+ // config app
+ ...defaultConfigApp,
+}
+
+export type AutopilotData = typeof defaultAutopilot
+export type ContractSetData = typeof defaultContractSet
+export type UploadPackingData = typeof defaultUploadPacking
+export type ConfigAppData = typeof defaultConfigApp
+export type GougingData = typeof defaultGouging
+export type RedundancyData = typeof defaultRedundancy
+export type SettingsData = typeof defaultValues
+
+// advanced defaults
+export const advancedDefaultAutopilot: AutopilotData = {
+ ...defaultAutopilot,
+ downloadTBMonth: new BigNumber(1),
+ uploadTBMonth: new BigNumber(1),
+ periodWeeks: new BigNumber(6),
+ renewWindowWeeks: new BigNumber(2),
+ amountHosts: new BigNumber(50),
+ autopilotContractSet: 'autopilot',
+ allowRedundantIPs: false,
+ maxDowntimeHours: new BigNumber(1440),
+ defragThreshold: new BigNumber(1000),
+}
+
+export const advancedDefaultContractSet: ContractSetData = {
+ ...defaultContractSet,
+ defaultContractSet: 'autopilot',
+}
+
+export const advancedDefaultConfigApp: ConfigAppData = {
+ ...defaultConfigApp,
+}
+
+export const advancedDefaultUploadPacking: UploadPackingData = {
+ ...defaultUploadPacking,
+}
+
+export const advancedDefaultGouging: GougingData = {
+ ...defaultGouging,
+}
+
+export const advancedDefaultRedundancy: RedundancyData = {
+ ...defaultRedundancy,
+}
diff --git a/apps/renterd/components/Autopilot/useSyncContractSet.tsx b/apps/renterd/contexts/config/useSyncContractSet.tsx
similarity index 93%
rename from apps/renterd/components/Autopilot/useSyncContractSet.tsx
rename to apps/renterd/contexts/config/useSyncContractSet.tsx
index b983f5786..fad4a909c 100644
--- a/apps/renterd/components/Autopilot/useSyncContractSet.tsx
+++ b/apps/renterd/contexts/config/useSyncContractSet.tsx
@@ -7,7 +7,7 @@ import {
import { useCallback } from 'react'
import { useSettingUpdate } from '@siafoundation/react-renterd'
import useLocalStorageState from 'use-local-storage-state'
-import { transformUpContractSet } from '../Config/transform'
+import { transformUpContractSet } from '../../contexts/config/transform'
import { useContractSetSettings } from '../../hooks/useContractSetSettings'
export function useSyncContractSet() {
@@ -38,7 +38,7 @@ export function useSyncContractSet() {
},
payload: transformUpContractSet(
{
- contractSet: autopilotContractSet,
+ defaultContractSet: autopilotContractSet,
},
contractSet.data
),
diff --git a/apps/renterd/contexts/contracts/index.tsx b/apps/renterd/contexts/contracts/index.tsx
index 32776d48a..1c98b6426 100644
--- a/apps/renterd/contexts/contracts/index.tsx
+++ b/apps/renterd/contexts/contracts/index.tsx
@@ -22,6 +22,7 @@ import {
} from './types'
import { columns } from './columns'
import { useSiaCentralHosts } from '@siafoundation/react-sia-central'
+import { useSyncStatus } from '../../hooks/useSyncStatus'
const defaultLimit = 50
@@ -141,6 +142,16 @@ function useContractsMain() {
filters
)
+ const syncStatus = useSyncStatus()
+ const datasetConfirmedCount = useMemo(() => {
+ if (!dataset) {
+ return 0
+ }
+ return dataset.filter(
+ (d) => d.contractHeightStart < syncStatus.nodeBlockHeight
+ ).length
+ }, [dataset, syncStatus.nodeBlockHeight])
+
return {
dataState,
limit,
@@ -149,6 +160,7 @@ function useContractsMain() {
error: response.error,
pageCount: datasetPage?.length || 0,
datasetCount: datasetFiltered?.length || 0,
+ datasetConfirmedCount,
columns: filteredTableColumns,
dataset,
datasetPage,
diff --git a/apps/renterd/contexts/files/index.tsx b/apps/renterd/contexts/files/index.tsx
index 319073d92..8d3c809a0 100644
--- a/apps/renterd/contexts/files/index.tsx
+++ b/apps/renterd/contexts/files/index.tsx
@@ -18,6 +18,7 @@ import { FullPath, FullPathSegments, pathSegmentsToPath } from './paths'
import { useUploads } from './uploads'
import { useDownloads } from './downloads'
import { useDataset } from './dataset'
+import { OnboardingBar } from '../../components/OnboardingBar'
function useFilesMain() {
const router = useRouter()
@@ -186,6 +187,7 @@ export function FilesProvider({ children }: Props) {
return (
{children}
+
)
diff --git a/apps/renterd/pages/autopilot/index.tsx b/apps/renterd/pages/autopilot/index.tsx
deleted file mode 100644
index ecc3bb184..000000000
--- a/apps/renterd/pages/autopilot/index.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Autopilot } from '../../components/Autopilot'
-
-export default function AutopilotPage() {
- return
-}
diff --git a/libs/design-system/src/app/AppNavbar.tsx b/libs/design-system/src/app/AppNavbar.tsx
index 06c3c8763..985dd3d8a 100644
--- a/libs/design-system/src/app/AppNavbar.tsx
+++ b/libs/design-system/src/app/AppNavbar.tsx
@@ -35,7 +35,7 @@ export function AppNavbar({ title, nav, stats, actions }: Props) {
{stats && (
-
+
{stats}
)}
diff --git a/libs/design-system/src/core/Heading.tsx b/libs/design-system/src/core/Heading.tsx
index a0a2e27ed..34fde4329 100644
--- a/libs/design-system/src/core/Heading.tsx
+++ b/libs/design-system/src/core/Heading.tsx
@@ -60,10 +60,10 @@ export const Heading = React.forwardRef<
: '')
return (
-
-
+
+
diff --git a/libs/design-system/src/form/ConfigurationPanel.tsx b/libs/design-system/src/form/ConfigurationPanel.tsx
index 131f4409a..83d6cf0e7 100644
--- a/libs/design-system/src/form/ConfigurationPanel.tsx
+++ b/libs/design-system/src/form/ConfigurationPanel.tsx
@@ -25,8 +25,13 @@ export function ConfigurationPanel<
Object.entries(fields) as [Path, ConfigField][]
).filter(
([_, val]) =>
- val.category === category && (!val.show || val.show(form.getValues()))
+ val.category === category &&
+ !val.hidden &&
+ (!val.show || val.show(form.getValues()))
)
+ if (list.length === 0) {
+ return null
+ }
return (
{list.map(([key, val], i) => (
diff --git a/libs/design-system/src/form/configurationFields.ts b/libs/design-system/src/form/configurationFields.ts
index a0666ca8e..e57334645 100644
--- a/libs/design-system/src/form/configurationFields.ts
+++ b/libs/design-system/src/form/configurationFields.ts
@@ -18,6 +18,7 @@ export type ConfigField<
type: 'number' | 'siacoin' | 'text' | 'password' | 'boolean' | 'select'
title: string
actions?: React.ReactNode
+ hidden?: boolean
category?: Categories
description?: React.ReactNode
units?: string
@@ -68,11 +69,13 @@ export function useRegisterForm<
const value = form.watch(name)
const error =
form.formState.touchedFields[name] && !!form.formState.errors[name]
+
const {
ref,
onChange: _onChange,
onBlur,
} = form.register(name, field.validation)
+
const onChange = useCallback(
(e: { target: unknown; type: unknown }) => {
_onChange(e)
diff --git a/libs/react-core/src/request.ts b/libs/react-core/src/request.ts
index defa30d73..448c763b1 100644
--- a/libs/react-core/src/request.ts
+++ b/libs/react-core/src/request.ts
@@ -19,12 +19,14 @@ export type HookArgsSwr<
> = Params extends void
? {
api?: string
+ standalone?: string
config?: RequestConfig
disabled?: boolean
}
: {
params: Params
api?: string
+ standalone?: string
config?: RequestConfig
disabled?: boolean
}
@@ -45,11 +47,13 @@ export type HookArgsWithPayloadSwr<
? Payload extends void
? {
api?: string
+ standalone?: string
config?: RequestConfig
disabled?: boolean
}
: {
api?: string
+ standalone?: string
payload: Payload
config?: RequestConfig
disabled?: boolean
@@ -58,6 +62,7 @@ export type HookArgsWithPayloadSwr<
? {
params: Params
api?: string
+ standalone?: string
config?: RequestConfig
disabled?: boolean
}
@@ -65,6 +70,7 @@ export type HookArgsWithPayloadSwr<
params: Params
payload: Payload
api?: string
+ standalone?: string
config?: RequestConfig
disabled?: boolean
}
diff --git a/libs/react-core/src/useGet.ts b/libs/react-core/src/useGet.ts
index 1eb718002..90359e093 100644
--- a/libs/react-core/src/useGet.ts
+++ b/libs/react-core/src/useGet.ts
@@ -32,7 +32,7 @@ export function useGetSwr(
)
return useSWR(
keyOrNull(
- reqRoute,
+ args.standalone ? `${args.standalone}/${reqRoute}` : reqRoute,
hookArgs.disabled || (passwordProtectRequestHooks && !settings.password)
),
async () => {