Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,53 @@ describe('Set Amulet Config Rules Form', () => {
expect(changes.length).toBe(2);
});

test('reward config minting scheme renders as a dropdown', async () => {
const user = userEvent.setup();

render(
<Wrapper>
<SetAmuletConfigRulesForm />
</Wrapper>
);

// Minting scheme should render as a Select, not a TextField
const mintingField = screen.getByTestId('config-field-rewardConfigMintingVersion');
expect(mintingField).toBeInTheDocument();
const selectInput = mintingField.querySelector('[role="combobox"]') as HTMLElement;
expect(selectInput).toBeInTheDocument();

// Open dropdown and verify options
await user.click(selectInput);
expect(screen.getByText('Featured App Markers (pre CIP-104)')).toBeInTheDocument();
expect(screen.getByText('Traffic-Based App Rewards (CIP-104)')).toBeInTheDocument();

// Select an option
await user.click(screen.getByText('Traffic-Based App Rewards (CIP-104)'));

// Verify current value is shown after change
const currentValue = screen.getByTestId('config-current-value-rewardConfigMintingVersion');
expect(currentValue).toBeInTheDocument();
});

test('reward config dry-run scheme includes None option', async () => {
const user = userEvent.setup();

render(
<Wrapper>
<SetAmuletConfigRulesForm />
</Wrapper>
);

const dryRunField = screen.getByTestId('config-field-rewardConfigDryRunVersion');
expect(dryRunField).toBeInTheDocument();
const selectInput = dryRunField.querySelector('[role="combobox"]') as HTMLElement;
expect(selectInput).toBeInTheDocument();

// Open dropdown and verify None option exists
await user.click(selectInput);
expect(screen.getByText('None (disabled)')).toBeInTheDocument();
});

test('should show proposal review page after form completion', async () => {
const user = userEvent.setup();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,19 +220,19 @@ describe('buildAmuletRulesConfigFromChanges', () => {
},
{
fieldName: 'rewardConfigMintingVersion',
label: 'Reward config: Minting version',
label: 'Reward config: Minting scheme',
currentValue: 'RewardVersion_FeaturedAppMarkers',
newValue: 'RewardVersion_TrafficBasedAppRewards',
},
{
fieldName: 'rewardConfigDryRunVersion',
label: 'Reward config: Dry-run version',
label: 'Reward config: Dry-run minting scheme',
currentValue: '',
newValue: 'RewardVersion_TrafficBasedAppRewards',
},
{
fieldName: 'rewardConfigBatchSize',
label: 'Reward config: Batch size',
label: 'Reward config: Merkle tree batch size',
currentValue: '100',
newValue: '200',
},
Expand Down
63 changes: 50 additions & 13 deletions apps/sv/frontend/src/components/form-components/ConfigField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
// SPDX-License-Identifier: Apache-2.0

import { Link as RouterLink } from 'react-router';
import { Box, Divider, TextField as MuiTextField, Typography } from '@mui/material';
import {
Box,
Divider,
FormControl,
MenuItem,
Select,
TextField as MuiTextField,
Typography,
} from '@mui/material';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useFieldContext } from '../../hooks/formContext';
Expand Down Expand Up @@ -90,18 +98,47 @@ export const ConfigField: React.FC<ConfigFieldProps> = props => {
</Box>

<Box sx={{ width: 250 }}>
<MuiTextField
{...textFieldProps}
// We choose empty string to represent fields that could be undefined because their values have not been set.
value={field.state.value?.value || ''}
onBlur={field.handleBlur}
onChange={e =>
field.handleChange({
fieldName: configChange.fieldName,
value: e.target.value,
})
}
/>
{configChange.options ? (
<FormControl size="small" fullWidth disabled={isDisabled}>
<Select
value={field.state.value?.value || ''}
onBlur={field.handleBlur}
onChange={e =>
field.handleChange({
fieldName: configChange.fieldName,
value: e.target.value,
})
}
data-testid={`config-field-${configChange.fieldName}`}
color={field.state.meta.isDefaultValue ? 'primary' : 'secondary'}
>
{configChange.options.map(option => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
) : (
<MuiTextField
{...textFieldProps}
// We choose empty string to represent fields that could be undefined because their values have not been set.
value={field.state.value?.value || ''}
onBlur={field.handleBlur}
onChange={e =>
field.handleChange({
fieldName: configChange.fieldName,
value: e.target.value,
})
}
/>
)}

{configChange.description && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
{configChange.description}
</Typography>
)}

{!field.state.meta.isDefaultValue && (
<Typography
Expand Down
25 changes: 22 additions & 3 deletions apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,40 +320,59 @@ function buildIssuanceCurveChanges(
return [...initialValues, ...futureValues];
}

const rewardVersionOptions = [
{
value: 'RewardVersion_FeaturedAppMarkers',
label: 'Featured App Markers (pre CIP-104)',
},
{
value: 'RewardVersion_TrafficBasedAppRewards',
label: 'Traffic-Based App Rewards (CIP-104)',
},
];

function buildRewardConfigChanges(
before: RewardConfig | null | undefined,
after: RewardConfig | null | undefined
) {
return [
{
fieldName: 'rewardConfigMintingVersion',
label: 'Reward config: Minting version',
label: 'Reward config: Minting scheme',
currentValue: before?.mintingVersion || '',
newValue: after?.mintingVersion || '',
options: rewardVersionOptions,
description: 'Which reward scheme to use for minting',
},
{
fieldName: 'rewardConfigDryRunVersion',
label: 'Reward config: Dry-run version',
label: 'Reward config: Dry-run minting scheme',
currentValue: before?.dryRunVersion || '',
newValue: after?.dryRunVersion || '',
options: [{ value: '', label: 'None (disabled)' }, ...rewardVersionOptions],
description:
'Which reward scheme to dry-run in parallel without minting. Leave empty to disable.',
},
{
fieldName: 'rewardConfigBatchSize',
label: 'Reward config: Batch size',
label: 'Reward config: Merkle tree batch size',
currentValue: before?.batchSize || '',
newValue: after?.batchSize || '',
description: 'Batch size for building the Merkle tree over minting allowances (default: 100)',
},
{
fieldName: 'rewardConfigRewardCouponTimeToLive',
label: 'Reward config: Reward coupon time to live (microseconds)',
currentValue: before?.rewardCouponTimeToLive.microseconds || '',
newValue: after?.rewardCouponTimeToLive.microseconds || '',
description: 'Time-to-live for RewardCouponV2 contracts (default: 36 hours)',
},
{
fieldName: 'rewardConfigAppRewardCouponThreshold',
label: 'Reward config: App reward coupon threshold ($)',
currentValue: before?.appRewardCouponThreshold || '',
newValue: after?.appRewardCouponThreshold || '',
description: 'Minimum reward amount in USD below which no coupon is created (default: $0.50)',
},
] as ConfigChange[];
}
Expand Down
8 changes: 8 additions & 0 deletions apps/sv/frontend/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ export interface ConfigChange {
* If the field should be disabled for editing.
*/
disabled?: boolean;
/**
* If set, render as a dropdown with these options instead of free text.
*/
options?: { value: string; label: string }[];
/**
* Optional description shown as help text below the field.
*/
description?: string;
}

export interface UpdateSvRewardWeightProposal {
Expand Down
Loading