Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI update/validation transaction #563

Merged
merged 11 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 10 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
17 changes: 12 additions & 5 deletions packages/browser-wallet/src/popup/popupX/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,19 @@ export const relativeRoutes = {
/** Configure existing delegator */
update: {
path: 'update',
/** Update validator stake */
stake: { path: 'stake' },
/** Update validator pool settings */
settings: { path: 'settings' },
/** Update validator keys */
keys: { path: 'keys' },
},
openPool: {
path: 'openPool',
},
keys: {
path: 'keys',
/** Submit configure validator transaction */
submit: {
path: 'submit',
config: {
backTitle: i18n.t('x:earn.validator.submit.backTitle'),
},
},
},
/** Delegation section */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import React, { useCallback, useMemo } from 'react';
import React, { useMemo } from 'react';
import {
AccountAddress,
AccountInfoDelegator,
AccountTransactionPayload,
AccountTransactionType,
CcdAmount,
ConfigureDelegationPayload,
DelegationTargetType,
TransactionHash,
} from '@concordium/web-sdk';
import { Navigate, useLocation, Location, useNavigate } from 'react-router-dom';
import { useAtomValue } from 'jotai';
import { useTranslation } from 'react-i18next';
import { useUpdateAtom } from 'jotai/utils';

import Button from '@popup/popupX/shared/Button';
import Page from '@popup/popupX/shared/Page';
Expand All @@ -21,70 +16,12 @@ import Card from '@popup/popupX/shared/Card';
import { ensureDefined } from '@shared/utils/basic-helpers';
import { useBlockChainParametersAboveV0 } from '@popup/shared/BlockChainParametersProvider';
import { secondsToDaysRoundedDown } from '@shared/utils/time-helpers';
import { grpcClientAtom } from '@popup/store/settings';
import { usePrivateKey } from '@popup/shared/utils/account-helpers';
import {
createPendingTransactionFromAccountTransaction,
getDefaultExpiry,
getTransactionAmount,
sendTransaction,
useGetTransactionFee,
} from '@popup/shared/utils/transaction-helpers';
import { addPendingTransactionAtom } from '@popup/store/transactions';
import { useGetTransactionFee, useTransactionSubmit } from '@popup/shared/utils/transaction-helpers';
import { cpStakingCooldown } from '@shared/utils/chain-parameters-helpers';
import { submittedTransactionRoute } from '@popup/popupX/constants/routes';
import Text from '@popup/popupX/shared/Text';
import { useSelectedAccountInfo } from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext';

enum TransactionSubmitErrorType {
InsufficientFunds = 'InsufficientFunds',
}

class TransactionSubmitError extends Error {
private constructor(public type: TransactionSubmitErrorType) {
super();
super.name = `TransactionSubmitError.${type}`;
}

public static insufficientFunds(): TransactionSubmitError {
return new TransactionSubmitError(TransactionSubmitErrorType.InsufficientFunds);
}
}

function useTransactionSubmit(sender: AccountAddress.Type, type: AccountTransactionType) {
const grpc = useAtomValue(grpcClientAtom);
const key = usePrivateKey(sender.address);
const addPendingTransaction = useUpdateAtom(addPendingTransactionAtom);

return useCallback(
async (payload: AccountTransactionPayload, cost: CcdAmount.Type) => {
const accountInfo = await grpc.getAccountInfo(sender);
if (
accountInfo.accountAvailableBalance.microCcdAmount <
getTransactionAmount(type, payload) + (cost.microCcdAmount || 0n)
) {
throw TransactionSubmitError.insufficientFunds();
}

const nonce = await grpc.getNextAccountNonce(sender);

const header = {
expiry: getDefaultExpiry(),
sender,
nonce: nonce.nonce,
};
const transaction = { payload, header, type };

const hash = await sendTransaction(grpc, transaction, key!);
const pending = createPendingTransactionFromAccountTransaction(transaction, hash, cost.microCcdAmount);
await addPendingTransaction(pending);

return hash;
},
[key]
);
}

export type DelegationResultLocationState = {
payload: ConfigureDelegationPayload;
type: 'register' | 'change' | 'remove';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.validator-commissions {
.page__top {
margin-bottom: 0 !important;
}

&,
&__form {
gap: rem(16px);
}

&__form {
display: flex;
flex-direction: column;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useCallback, useMemo } from 'react';
import Page from '@popup/popupX/shared/Page';
import { useTranslation } from 'react-i18next';
import Text from '@popup/popupX/shared/Text';
import Button from '@popup/popupX/shared/Button';
import { ChainParameters, ChainParametersV0, CommissionRange, CommissionRates } from '@concordium/web-sdk';
import Form, { useForm } from '@popup/popupX/shared/Form';
import FormSlider from '@popup/popupX/shared/Form/Slider/Slider';
import { PropsOf } from 'wallet-common-helpers';
import { isRange } from '../util';

const COMMISSION_STEP = 0.001;

type Props = {
initial?: CommissionRates;
onSubmit(values: CommissionRates): void;
chainParams: Exclude<ChainParameters, ChainParametersV0>;
};

export default function Commissions({ initial, onSubmit, chainParams }: Props) {
const { t } = useTranslation('x', { keyPrefix: 'earn.validator.commissions' });
const { bakingCommissionRange, finalizationCommissionRange, transactionCommissionRange } = chainParams;
const defaultValues: CommissionRates = useMemo(
() => ({
bakingCommission: (initial?.bakingCommission ?? bakingCommissionRange.min) * 100,
transactionCommission: (initial?.transactionCommission ?? transactionCommissionRange.min) * 100,
finalizationCommission: (initial?.finalizationCommission ?? finalizationCommissionRange.min) * 100,
}),
[initial, chainParams]
);
const form = useForm({ defaultValues });

const commissionRules = useCallback(
(range: CommissionRange): PropsOf<typeof FormSlider>['rules'] => {
const min = range.min * 100;
const max = range.max * 100;
return {
min: { value: min, message: t('error.min', { min }) },
max: { value: max, message: t('error.max', { max }) },
required: t('error.required'),
};
},
[t]
);

const handleSubmit = useCallback(
(values: CommissionRates) => {
const fractions: CommissionRates = {
transactionCommission: values.transactionCommission / 100,
bakingCommission: values.bakingCommission / 100,
finalizationCommission: values.finalizationCommission / 100,
};

onSubmit(fractions);
},
[onSubmit]
);

return (
<Page className="validator-commissions">
<Page.Top heading={t('title')} />
<Text.Capture>{t('desciption')}</Text.Capture>
<Form formMethods={form} onSubmit={handleSubmit} className="validator-commissions__form">
{(f) => (
<>
{isRange(transactionCommissionRange) && (
<FormSlider
control={f.control}
name="transactionCommission"
label={t('fieldTransactionFee.label')}
min={transactionCommissionRange.min * 100}
max={transactionCommissionRange.max * 100}
rules={commissionRules(transactionCommissionRange)}
step={COMMISSION_STEP}
unit="%"
/>
)}
{isRange(bakingCommissionRange) && (
<FormSlider
control={f.control}
name="bakingCommission"
label={t('fieldBlockReward.label')}
min={bakingCommissionRange.min * 100}
max={bakingCommissionRange.max * 100}
rules={commissionRules(bakingCommissionRange)}
step={COMMISSION_STEP}
unit="%"
/>
)}
{isRange(finalizationCommissionRange) && (
<FormSlider
control={f.control}
name="finalizationCommission"
label={t('fieldFinalizationReward.label')}
min={finalizationCommissionRange.min * 100}
max={finalizationCommissionRange.max * 100}
rules={commissionRules(finalizationCommissionRange)}
step={COMMISSION_STEP}
unit="%"
/>
)}
</>
)}
</Form>
<Page.Footer>
<Button.Main label={t('buttonContinue')} onClick={form.handleSubmit(handleSubmit)} />
</Page.Footer>
</Page>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Commissions';
Original file line number Diff line number Diff line change
@@ -1,47 +1,29 @@
.validator-keys-container {
.capture__main_small {
color: $color-white;
word-wrap: break-word;
.validator-keys {
gap: rem(16px);

.page__top {
margin-bottom: 0 !important;
}

.validator-keys {
&__title {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: rem(12px);
&--expanded {
.validator-keys__expand svg {
transform: rotate(-90deg);
}
}

&__card {
display: flex;
flex-direction: column;
border-radius: rem(12px);
padding: rem(16px);
margin-top: rem(16px);
background-color: rgba($color-grey-3, 0.3);

&_row:not(:last-child) {
padding-bottom: rem(8px);
margin-bottom: rem(8px);
border-bottom: 1px solid $color-grey-3;
}

&_row {
display: flex;
flex-direction: column;

.capture__main_small:first-child {
color: rgba($color-mineral-3, 0.5);
margin-bottom: rem(6px);
}
}
&:not(.validator-keys--expanded) {
.card-x .details .capture__main_small {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}

&__expand {
margin-bottom: rem(8px);

&__export {
display: flex;
align-items: center;
gap: rem(8px);
margin-top: rem(16px);
svg {
transform: rotate(90deg);
}
}
}
Loading
Loading