Skip to content
Closed
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 @@ -137,7 +137,7 @@ class LocalNetDedicatedSyncIntegrationTest extends IntegrationTestWithIsolatedEn
new DsoRules_RegisterSynchronizer(
appSynchronizerId.toProtoPrimitive,
operatorParty.toProtoPrimitive,
new GovernanceParameters(java.math.BigDecimal.ONE.setScale(10)),
new GovernanceParameters(java.math.BigDecimal.ONE.setScale(10), 0L),
)
)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class SyncOperatorTrafficIntegrationTest
.exerciseDsoRules_RegisterSynchronizer(
synchronizerId.toProtoPrimitive,
operatorParty.toProtoPrimitive,
new GovernanceParameters(java.math.BigDecimal.ONE.setScale(10)),
new GovernanceParameters(java.math.BigDecimal.ONE.setScale(10), 0L),
)
.commands
.asScala
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1783,7 +1783,7 @@ trait AmuletTransferUtil { self: StoreTestBase =>
dsoParty.toProtoPrimitive,
synchronizerId,
operator.toProtoPrimitive,
new GovernanceParameters(java.math.BigDecimal.ONE.setScale(10)),
new GovernanceParameters(java.math.BigDecimal.ONE.setScale(10), 0L),
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,43 @@ describe('Register Dedicated Synchronizer Form', () => {
await user.click(screen.getByTestId('register-synchronizer-action'));
expect(screen.queryByText('Must be greater than 0 and at most 1')).not.toBeInTheDocument();
});

test('prefills the outage advance with the no-advance value', () => {
render(
<Wrapper>
<RegisterSynchronizerForm />
</Wrapper>
);

expect(screen.getByTestId('register-synchronizer-outage-advance').getAttribute('value')).toBe(
'0'
);
});

test('accepts only a whole number of bytes for the outage advance', async () => {
const user = userEvent.setup();
render(
<Wrapper>
<RegisterSynchronizerForm />
</Wrapper>
);

const advanceInput = screen.getByTestId('register-synchronizer-outage-advance');
const message = 'Must be a whole number of bytes, 0 or more';

// the template's ensure rejects a negative advance, and Daml's Int has no fraction
for (const bad of ['-1', '1.5']) {
await user.clear(advanceInput);
await user.type(advanceInput, bad);
await user.click(screen.getByTestId('register-synchronizer-action'));
screen.getByText(message);
}

await user.clear(advanceInput);
await user.type(advanceInput, '5000000');
await user.click(screen.getByTestId('register-synchronizer-action'));
expect(screen.queryByText(message)).not.toBeInTheDocument();
});
});

describe('Register Dedicated Synchronizer Form, duplicate registration', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '../../utils/constants';
import {
validateDiscountFactor,
validateOutageAdvance,
validateEffectiveDate,
validateExpiration,
validateExpiryEffectiveDate,
Expand All @@ -42,13 +43,18 @@ interface ExtraFormField {
operator: string;
/** Stated on every registration: 1 is the no-discount value, not an absent one. */
discountFactor: string;
/** Bytes per member; 0 is the no-advance value. */
outageAdvance: string;
}

export type RegisterSynchronizerFormData = CommonProposalFormData & ExtraFormField;

/** The no-discount value. A registration states its parameters rather than defaulting them. */
const DEFAULT_DISCOUNT_FACTOR = '1.0';

/** The no-advance value. */
const DEFAULT_OUTAGE_ADVANCE = '0';

export const RegisterSynchronizerForm: React.FC = _ => {
const dsoInfosQuery = useDsoInfos();
const initialExpiration = getInitialExpiration(dsoInfosQuery.data);
Expand All @@ -74,13 +80,15 @@ export const RegisterSynchronizerForm: React.FC = _ => {
synchronizerId: '',
operator: '',
discountFactor: DEFAULT_DISCOUNT_FACTOR,
outageAdvance: DEFAULT_OUTAGE_ADVANCE,
};

const form = useAppForm({
defaultValues,

onSubmit: async ({ value }) => {
const discountFactor = value.discountFactor.trim();
const outageAdvance = value.outageAdvance.trim();
const action: ActionRequiringConfirmation = {
tag: 'ARC_DsoRules',
value: {
Expand All @@ -91,7 +99,7 @@ export const RegisterSynchronizerForm: React.FC = _ => {
operator: value.operator,
// The same vote sets the parameters, so a synchronizer promised a discount is
// never registered at the full price while a second vote is arranged.
governanceParameters: { discountFactor },
governanceParameters: { discountFactor, outageAdvance },
},
},
},
Expand Down Expand Up @@ -166,6 +174,7 @@ export const RegisterSynchronizerForm: React.FC = _ => {
synchronizerId={form.state.values.synchronizerId}
operator={form.state.values.operator}
discountFactor={form.state.values.discountFactor.trim()}
outageAdvance={form.state.values.outageAdvance.trim()}
onEdit={() => setShowConfirmation(false)}
onSubmit={() => {}}
/>
Expand Down Expand Up @@ -231,6 +240,22 @@ export const RegisterSynchronizerForm: React.FC = _ => {
)}
</form.AppField>

<form.AppField
name="outageAdvance"
validators={{
onBlur: ({ value }) => validateOutageAdvance(value.trim()),
onChange: ({ value }) => validateOutageAdvance(value.trim()),
}}
>
{field => (
<field.TextField
title="Outage Traffic Advance"
id="register-synchronizer-outage-advance"
subtitle="Bytes each member may use beyond what it has bought while the operator is cut off from the global synchronizer, taken back when it reconnects. 0 means no advance."
/>
)}
</form.AppField>

<form.AppField
name="expiryDate"
validators={{
Expand Down
11 changes: 11 additions & 0 deletions apps/sv/frontend/src/components/forms/formValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ export const discountFactorSchema = z
message: 'Must be greater than 0 and at most 1',
});

// Bytes of traffic per member; the template rejects a negative amount and 0 means no advance.
export const outageAdvanceSchema = z
.string()
.min(1, { message: 'Required' })
.regex(/^\d+$/, { message: 'Must be a whole number of bytes, 0 or more' });

export const svWeightSchema = z
.string()
.min(1, { message: 'Weight is required' })
Expand Down Expand Up @@ -228,6 +234,11 @@ export const validateDiscountFactor = (value: string): string | false => {
return result.success ? false : result.error.issues[0].message;
};

export const validateOutageAdvance = (value: string): string | false => {
const result = outageAdvanceSchema.safeParse(value);
return result.success ? false : result.error.issues[0].message;
};

export const validatePartyId = (value: string): string | false => {
const result = partyIdSchema.safeParse(value);
return result.success ? false : result.error.issues[0].message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ export const ProposalDetailsContent: React.FC<ProposalDetailsContentProps> = pro
synchronizerId={proposalDetails.proposal.synchronizerId}
operator={proposalDetails.proposal.operator}
discountFactor={proposalDetails.proposal.discountFactor}
outageAdvance={proposalDetails.proposal.outageAdvance}
/>
)}

Expand Down Expand Up @@ -963,12 +964,14 @@ interface RegisterSynchronizerSectionProps {
synchronizerId: string;
operator: string;
discountFactor: string;
outageAdvance: string;
}

const RegisterSynchronizerSection = ({
synchronizerId,
operator,
discountFactor,
outageAdvance,
}: RegisterSynchronizerSectionProps) => {
return (
<Box
Expand Down Expand Up @@ -1003,6 +1006,16 @@ const RegisterSynchronizerSection = ({
</Typography>
}
/>
<DetailItem
label="Outage Traffic Advance"
value={
<Typography variant="body1" data-testid="proposal-details-outage-advance">
{outageAdvance === '0'
? 'None'
: `${outageAdvance} bytes per member while the global synchronizer is unreachable`}
</Typography>
}
/>
</Box>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type ProposalSummaryProps = BaseProposalSummaryProps &
synchronizerId: string;
operator: string;
discountFactor: string;
outageAdvance: string;
}
| {
formType: 'update-right-weight';
Expand Down Expand Up @@ -260,6 +261,11 @@ export const ProposalSummary: React.FC<ProposalSummaryProps> = props => {
label="Traffic Discount"
value={props.discountFactor}
/>
<ProposalReviewField
id="outageAdvance"
label="Outage Traffic Advance"
value={props.outageAdvance}
/>
</>
)}

Expand Down
7 changes: 5 additions & 2 deletions apps/sv/frontend/src/utils/governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ export function buildProposal(action: ActionRequiringConfirmation, dsoInfo?: Dso
return createRegisterSynchronizerProposal(
dsoAction.value.synchronizerId,
dsoAction.value.operator,
dsoAction.value.governanceParameters.discountFactor
dsoAction.value.governanceParameters.discountFactor,
dsoAction.value.governanceParameters.outageAdvance
);
case 'SRARC_UpdateSvRewardWeight': {
const allSvInfos = dsoInfo?.dsoRules.payload.svs.entriesArray() || [];
Expand Down Expand Up @@ -268,12 +269,14 @@ function createRevokeFeatureAppProposal(rightContractId: string): UnfeatureAppPr
function createRegisterSynchronizerProposal(
synchronizerId: string,
operator: string,
discountFactor: string
discountFactor: string,
outageAdvance: string
): RegisterSynchronizerProposal {
return {
synchronizerId: synchronizerId,
operator: operator,
discountFactor: discountFactor,
outageAdvance: outageAdvance,
};
}

Expand Down
2 changes: 2 additions & 0 deletions apps/sv/frontend/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export interface RegisterSynchronizerProposal {
operator: string;
/** The discount factor the registration would be created with. */
discountFactor: string;
/** The per-member outage traffic advance, in bytes, the registration would be created with. */
outageAdvance: string;
}

export interface UpdateSvRewardWeightProposal {
Expand Down
Loading
Loading