Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pin-all-prices-or-none.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'hostd': minor
---

Price pinning is now all or nothing - the per-price pin toggles have been replaced with a single Pin prices switch that pins storage price, egress price, ingress price, and max collateral together. Hosts that currently have only some of their prices pinned keep that configuration until they change pinning themselves.
27 changes: 8 additions & 19 deletions apps/hostd-e2e/src/fixtures/configResetAllSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,20 @@ export const configResetAllSettings = step(
await fillSelectInputByName(page, 'pinnedCurrency', 'USD')
await fillTextInputByName(page, 'pinnedThreshold', '3')

await setSwitchByLabel(page, 'shouldPinStoragePrice', false)
await fillTextInputByName(page, 'storagePrice', '10')
await setSwitchByLabel(page, 'shouldPinStoragePrice', true)
// Pinning is all or nothing - set the pinned values, then turn pinning
// off so the siacoin values are the ones in effect.
await setSwitchByLabel(page, 'shouldPinPrices', true)
await fillTextInputByName(page, 'storagePricePinned', '5')
await setSwitchByLabel(page, 'shouldPinStoragePrice', false)

await setSwitchByLabel(page, 'shouldPinEgressPrice', false)
await fillTextInputByName(page, 'egressPrice', '10')
await setSwitchByLabel(page, 'shouldPinEgressPrice', true)
await fillTextInputByName(page, 'egressPricePinned', '5')
await setSwitchByLabel(page, 'shouldPinEgressPrice', false)

await setSwitchByLabel(page, 'shouldPinIngressPrice', false)
await fillTextInputByName(page, 'ingressPrice', '10')
await setSwitchByLabel(page, 'shouldPinIngressPrice', true)
await fillTextInputByName(page, 'ingressPricePinned', '5')
await setSwitchByLabel(page, 'shouldPinIngressPrice', false)
await fillTextInputByName(page, 'maxCollateralPinned', '5')
await setSwitchByLabel(page, 'shouldPinPrices', false)

await fillTextInputByName(page, 'storagePrice', '10')
await fillTextInputByName(page, 'egressPrice', '10')
await fillTextInputByName(page, 'ingressPrice', '10')
await fillTextInputByName(page, 'collateralMultiplier', '2')

await setSwitchByLabel(page, 'shouldPinMaxCollateral', false)
await fillTextInputByName(page, 'maxCollateral', '10')
await setSwitchByLabel(page, 'shouldPinMaxCollateral', true)
await fillTextInputByName(page, 'maxCollateralPinned', '5')
await setSwitchByLabel(page, 'shouldPinMaxCollateral', false)

await fillTextInputByName(page, 'contractPrice', '0.2')
await fillTextInputByName(page, 'baseRPCPrice', '1')
Expand Down
59 changes: 59 additions & 0 deletions apps/hostd-e2e/src/fixtures/configSetMixedPinning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Axios from 'axios'
import { Page } from '@playwright/test'
import { clusterd } from '@siafoundation/clusterd'
import { step } from '@siafoundation/e2e'

/**
* Sets a pinning configuration where only some prices are pinned, which the UI
* no longer allows the host to create. Hosts configured before pinning became
* all or nothing can still be in this state, so it is set directly on the
* daemon to test how the UI presents and resolves it.
*/
export const configSetMixedPinning = step(
'set mixed pinning configuration',
async ({ page }: { page: Page }) => {
const hostdNode = clusterd.nodes.find((n) => n.type === 'hostd')
await Axios.put(
`${hostdNode.apiAddress}/api/settings/pinned`,
{
currency: 'usd',
threshold: 0.02,
storage: { pinned: true, value: 5 },
egress: { pinned: false, value: 0 },
ingress: { pinned: false, value: 0 },
maxCollateral: { pinned: false, value: 0 },
},
{
auth: { username: '', password: hostdNode.password },
timeout: 10_000,
},
)
// The app has already fetched and cached the settings during login, so
// reload to pick up the configuration that was just set on the daemon.
await page.reload()
},
)

/**
* The pinning configuration as reported by the daemon, for asserting what was
* actually saved rather than what the UI displays.
*/
export const getPinnedSettings = step(
'get pinned settings',
async (): Promise<{
storage: { pinned: boolean }
egress: { pinned: boolean }
ingress: { pinned: boolean }
maxCollateral: { pinned: boolean }
}> => {
const hostdNode = clusterd.nodes.find((n) => n.type === 'hostd')
const response = await Axios.get(
`${hostdNode.apiAddress}/api/settings/pinned`,
{
auth: { username: '', password: hostdNode.password },
timeout: 10_000,
},
)
return response.data
},
)
65 changes: 49 additions & 16 deletions apps/hostd-e2e/src/specs/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { test, expect } from '@playwright/test'
import {
setViewMode,
expectTextInputByName,
expectTextInputVisible,
expectTextInputNotVisible,
fillTextInputByName,
fillSelectInputByName,
Expand Down Expand Up @@ -32,9 +33,9 @@ test('basic field change and save behaviour', async ({ page }) => {
await fillTextInputByName(page, 'maxContractDuration', '7')
await fillSelectInputByName(page, 'pinnedCurrency', 'BTC')
await fillTextInputByName(page, 'pinnedThreshold', '7')
await setSwitchByLabel(page, 'shouldPinStoragePrice', true)
await setSwitchByLabel(page, 'shouldPinPrices', true)
await fillTextInputByName(page, 'storagePricePinned', '77')
await fillTextInputByName(page, 'egressPrice', '77')
await fillTextInputByName(page, 'egressPricePinned', '77')
await fillTextInputByName(page, 'baseRPCPrice', '77')

// Correct number of changes is shown.
Expand All @@ -52,30 +53,62 @@ test('basic field change and save behaviour', async ({ page }) => {
await expectTextInputByName(page, 'maxContractDuration', '7')
await fillSelectInputByName(page, 'pinnedCurrency', 'USD')
await expectTextInputByName(page, 'pinnedThreshold', '7')
// Pinned vs not pinned fields correctly shown or hidden.
await expectSwitchByLabel(page, 'shouldPinStoragePrice', true)
// Pinning applies to every price at once, so all of the pinned fields are
// shown and none of the siacoin fields are.
await expectSwitchByLabel(page, 'shouldPinPrices', true)
await expectTextInputByName(page, 'storagePricePinned', '$77')
await expectTextInputNotVisible(page, 'storagePrice')
await expectSwitchByLabel(page, 'shouldPinEgressPrice', false)
await expectTextInputByName(page, 'egressPrice', '77')
await expectTextInputNotVisible(page, 'egressPricePinned')
await expectTextInputByName(page, 'egressPricePinned', '$77')
await expectTextInputNotVisible(page, 'egressPrice')
await expectTextInputNotVisible(page, 'ingressPrice')
await expectTextInputNotVisible(page, 'maxCollateral')
await expectTextInputByName(page, 'baseRPCPrice', '77')
})

test('pin switches should show in both view modes', async ({ page }) => {
test('pinning applies to all prices at once', async ({ page }) => {
await navigateToConfig({ page })
await setViewMode({ page, state: 'advanced' })

// With pinning off every price is configured in siacoin.
await setSwitchByLabel(page, 'shouldPinPrices', false)
await expectTextInputVisible(page, 'storagePrice')
await expectTextInputVisible(page, 'egressPrice')
await expectTextInputVisible(page, 'ingressPrice')
await expectTextInputVisible(page, 'maxCollateral')
await expectTextInputNotVisible(page, 'storagePricePinned')

// Turning pinning on switches all of them to fiat.
await setSwitchByLabel(page, 'shouldPinPrices', true)
await expectTextInputVisible(page, 'storagePricePinned')
await expectTextInputVisible(page, 'egressPricePinned')
await expectTextInputVisible(page, 'ingressPricePinned')
await expectTextInputVisible(page, 'maxCollateralPinned')
await expectTextInputNotVisible(page, 'storagePrice')
})

test('pin switch should show in both view modes', async ({ page }) => {
await navigateToConfig({ page })
await setViewMode({ page, state: 'basic' })
await expectSwitchVisible(page, 'shouldPinPrices')

await navigateToConfig({ page })
await setViewMode({ page, state: 'advanced' })
await expectSwitchVisible(page, 'shouldPinPrices')
})

test('pinned max collateral should show in both view modes', async ({
page,
}) => {
// Max collateral must be configurable wherever pinning is, otherwise it can
// be pinned without the host being able to see or set its value.
await navigateToConfig({ page })
await setViewMode({ page, state: 'basic' })
await expectSwitchVisible(page, 'shouldPinStoragePrice')
await expectSwitchVisible(page, 'shouldPinEgressPrice')
await expectSwitchVisible(page, 'shouldPinIngressPrice')
await expectSwitchVisible(page, 'shouldPinMaxCollateral')
await setSwitchByLabel(page, 'shouldPinPrices', true)
await expectTextInputVisible(page, 'maxCollateralPinned')

await navigateToConfig({ page })
await setViewMode({ page, state: 'advanced' })
await expectSwitchVisible(page, 'shouldPinStoragePrice')
await expectSwitchVisible(page, 'shouldPinEgressPrice')
await expectSwitchVisible(page, 'shouldPinIngressPrice')
await expectSwitchVisible(page, 'shouldPinMaxCollateral')
await expectTextInputVisible(page, 'maxCollateralPinned')
})

test('dynamic max collateral suggestion', async ({ page }) => {
Expand Down
134 changes: 134 additions & 0 deletions apps/hostd-e2e/src/specs/configLegacyPinning.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { test, expect } from '@playwright/test'
import {
setViewMode,
expectTextInputVisible,
expectTextInputNotVisible,
expectSwitchByLabel,
fillTextInputByName,
} from '@siafoundation/e2e'
import { navigateToConfig } from '../fixtures/navigate'
import { afterTest, beforeTest } from '../fixtures/beforeTest'
import {
configSetMixedPinning,
getPinnedSettings,
} from '../fixtures/configSetMixedPinning'

// Pinning is all or nothing, but hosts configured before that was enforced can
// still have only some of their prices pinned. That configuration is preserved
// until the host changes pinning themselves.
test.beforeEach(async ({ page }) => {
await beforeTest(page)
await configSetMixedPinning({ page })
})

test.afterEach(async () => {
await afterTest()
})

test('a mixed pinning configuration is shown as unpinned with a notice', async ({
page,
}) => {
await navigateToConfig({ page })
await setViewMode({ page, state: 'advanced' })

// The toggle reads off because pinning is not in effect for every price.
await expectSwitchByLabel(page, 'shouldPinPrices', false)
await expect(
page.getByText('Some of your prices are pinned to fiat values'),
).toBeVisible()

// Each price still shows the value that is actually in effect for it, so the
// pinned one shows its fiat input and the rest show siacoin.
await expectTextInputVisible(page, 'storagePricePinned')
await expectTextInputNotVisible(page, 'storagePrice')
await expectTextInputVisible(page, 'egressPrice')
await expectTextInputVisible(page, 'ingressPrice')
await expectTextInputVisible(page, 'maxCollateral')

// The pinned price is called out.
await expect(
page.getByTestId('storagePriceGroup').getByText('Pinned'),
).toBeVisible()
await expect(
page.getByTestId('egressPriceGroup').getByText('Pinned'),
).toBeHidden()
})

test('a mixed pinning configuration survives an unrelated change', async ({
page,
}) => {
await navigateToConfig({ page })
await setViewMode({ page, state: 'advanced' })

// Change something that has nothing to do with pinning and save.
await fillTextInputByName(page, 'baseRPCPrice', '55')
await page.getByText('Save changes').click()
await expect(page.getByText('1 change')).toBeHidden()

// The host's existing pinning configuration is untouched.
const settings = await getPinnedSettings()
expect(settings.storage.pinned).toBe(true)
expect(settings.egress.pinned).toBe(false)
expect(settings.ingress.pinned).toBe(false)
expect(settings.maxCollateral.pinned).toBe(false)

// And the notice is still shown after the save.
await expect(
page.getByText('Some of your prices are pinned to fiat values'),
).toBeVisible()
})

test('turning pinning on resolves a mixed configuration to all pinned', async ({
page,
}) => {
await navigateToConfig({ page })
await setViewMode({ page, state: 'advanced' })

await page.getByRole('button', { name: 'Pin all', exact: true }).click()

// Resolving the mixed configuration takes the notice away and counts as a
// change on its own, so the host can save the choice immediately.
await expect(
page.getByText('Some of your prices are pinned to fiat values'),
).toBeHidden()
await expect(page.getByText('Save changes')).toBeEnabled()

// Every price now needs a fiat value.
await fillTextInputByName(page, 'egressPricePinned', '2')
await fillTextInputByName(page, 'ingressPricePinned', '1')
await fillTextInputByName(page, 'maxCollateralPinned', '100')
await page.getByText('Save changes').click()

await expect(async () => {
const settings = await getPinnedSettings()
expect(settings.storage.pinned).toBe(true)
expect(settings.egress.pinned).toBe(true)
expect(settings.ingress.pinned).toBe(true)
expect(settings.maxCollateral.pinned).toBe(true)
}).toPass()
})

test('turning pinning off resolves a mixed configuration to none pinned', async ({
page,
}) => {
await navigateToConfig({ page })
await setViewMode({ page, state: 'advanced' })

// Unpinning is a single click even though the switch already reads off.
await page.getByRole('button', { name: 'Unpin all' }).click()

await expect(
page.getByText('Some of your prices are pinned to fiat values'),
).toBeHidden()
// The choice is a change on its own, so no other edit is needed to save it.
await expect(page.getByText('Save changes')).toBeEnabled()
await page.getByText('Save changes').click()

await expect(async () => {
const settings = await getPinnedSettings()
expect(settings.storage.pinned).toBe(false)
expect(settings.egress.pinned).toBe(false)
expect(settings.ingress.pinned).toBe(false)
expect(settings.maxCollateral.pinned).toBe(false)
}).toPass()
})
Loading
Loading