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
109 changes: 109 additions & 0 deletions packages/page-staking-async/src/Actions/Account/BondExtra.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { ApiPromise } from '@polkadot/api';
import type { DeriveBalancesAll, DeriveStakingAccount } from '@polkadot/api-derive/types';
import type { AmountValidateState } from '../types.js';

import React, { useMemo, useState } from 'react';

import { InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { BalanceFree } from '@polkadot/react-query';
import { BN, BN_ZERO } from '@polkadot/util';

import { useTranslation } from '../../translate.js';
import ValidateAmount from './InputValidateAmount.js';

interface Props {
controllerId: string | null;
onClose: () => void;
stakingInfo?: DeriveStakingAccount;
stashId: string;
}

function calcBalance (api: ApiPromise, stakingInfo?: DeriveStakingAccount, stashBalance?: DeriveBalancesAll): BN | null {
if (stakingInfo?.stakingLedger && stashBalance) {
const sumUnlocking = (stakingInfo.unlocking || []).reduce((acc, { value }) => acc.iadd(value), new BN(0));
const redeemable = stakingInfo.redeemable || BN_ZERO;
const available = stashBalance.freeBalance.sub(stakingInfo.stakingLedger.active?.unwrap() || BN_ZERO).sub(sumUnlocking).sub(redeemable);

return available.gt(api.consts.balances.existentialDeposit)
? available.sub(api.consts.balances.existentialDeposit)
: BN_ZERO;
}

return null;
}

function BondExtra ({ controllerId, onClose, stakingInfo, stashId }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [amountError, setAmountError] = useState<AmountValidateState | null>(null);
const [maxAdditional, setMaxAdditional] = useState<BN | undefined>();
const stashBalance = useCall<DeriveBalancesAll>(api.derive.balances?.all, [stashId]);
const currentAmount = useMemo(
() => stakingInfo?.stakingLedger?.active?.unwrap(),
[stakingInfo]
);

const startBalance = useMemo(
() => calcBalance(api, stakingInfo, stashBalance),
[api, stakingInfo, stashBalance]
);

return (
<Modal
header= {t('Bond more funds')}
onClose={onClose}
size='large'
>
<Modal.Content>
<Modal.Columns hint={t('Since this transaction deals with funding, the stash account will be used.')}>
<InputAddress
defaultValue={stashId}
isDisabled
label={t('stash account')}
/>
</Modal.Columns>
{startBalance && (
<Modal.Columns hint={t('The amount placed at-stake should allow some free funds for future transactions.')}>
<InputBalance
autoFocus
defaultValue={startBalance}
isError={!!amountError?.error || !maxAdditional || maxAdditional.isZero()}
label={t('additional funds to bond')}
labelExtra={
<BalanceFree
label={<span className='label'>{t('balance')}</span>}
params={stashId}
/>
}
onChange={setMaxAdditional}
/>
<ValidateAmount
controllerId={controllerId}
currentAmount={currentAmount}
onError={setAmountError}
stashId={stashId}
value={maxAdditional}
/>
</Modal.Columns>
)}
</Modal.Content>
<Modal.Actions>
<TxButton
accountId={stashId}
icon='sign-in-alt'
isDisabled={!maxAdditional?.gt(BN_ZERO) || !!amountError?.error}
label={t('Bond more')}
onStart={onClose}
params={[maxAdditional]}
tx={api.tx.staking.bondExtra}
/>
</Modal.Actions>
</Modal>
);
}

export default React.memo(BondExtra);
137 changes: 137 additions & 0 deletions packages/page-staking-async/src/Actions/Account/InjectKeys.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { KeypairType } from '@polkadot/util-crypto/types';

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { Button, Dropdown, Input, MarkWarning, Modal } from '@polkadot/react-components';
import { useQueue } from '@polkadot/react-hooks';
import { keyring } from '@polkadot/ui-keyring';
import { assert, u8aToHex } from '@polkadot/util';
import { keyExtractSuri, mnemonicValidate } from '@polkadot/util-crypto';

import { useTranslation } from '../../translate.js';

interface Props {
onClose: () => void;
}

const CRYPTO_MAP: Record<string, KeypairType[]> = {
aura: ['ed25519', 'sr25519'],
babe: ['sr25519'],
gran: ['ed25519'],
imon: ['ed25519', 'sr25519'],
para: ['sr25519']
};

const EMPTY_KEY = '0x';

function InjectKeys ({ onClose }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { queueRpc } = useQueue();

// this needs to align with what is set as the first value in `type`
const [crypto, setCrypto] = useState<KeypairType>('sr25519');
const [publicKey, setPublicKey] = useState(EMPTY_KEY);
const [suri, setSuri] = useState('');
const [keyType, setKeyType] = useState('babe');

const keyTypeOptRef = useRef([
{ text: t('Aura'), value: 'aura' },
{ text: t('Babe'), value: 'babe' },
{ text: t('Grandpa'), value: 'gran' },
{ text: t('I\'m Online'), value: 'imon' },
{ text: t('Parachains'), value: 'para' }
]);

useEffect((): void => {
setCrypto(CRYPTO_MAP[keyType][0]);
}, [keyType]);

useEffect((): void => {
try {
const { phrase } = keyExtractSuri(suri);

assert(mnemonicValidate(phrase), 'Invalid mnemonic phrase');

setPublicKey(u8aToHex(keyring.createFromUri(suri, {}, crypto).publicKey));
} catch {
setPublicKey(EMPTY_KEY);
}
}, [crypto, suri]);

const _onSubmit = useCallback(
(): void => queueRpc({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
rpc: { method: 'insertKey', section: 'author' } as any,
values: [keyType, suri, publicKey]
}),
[keyType, publicKey, queueRpc, suri]
);

const _cryptoOptions = useMemo(
() => CRYPTO_MAP[keyType].map((value): { text: string; value: KeypairType } => ({
text: value === 'ed25519'
? t('ed25519, Edwards')
: t('sr15519, Schnorrkel'),
value
})),
[keyType, t]
);

return (
<Modal
header={t('Inject Keys')}
onClose={onClose}
size='large'
>
<Modal.Content>
<Modal.Columns>
<MarkWarning content={t('This operation will be performed on the relay chain.')} />
</Modal.Columns>
<Modal.Columns hint={t('The seed and derivation path will be submitted to the validator node. this is an advanced operation, only to be performed when you are sure of the security and connection risks.')}>
<Input
autoFocus
isError={publicKey.length !== 66}
label={t('suri (seed & derivation)')}
onChange={setSuri}
value={suri}
/>
<MarkWarning content={t('This operation will submit the seed via an RPC call. Do not perform this operation on a public RPC node, but ensure that the node is local, connected to your validator and secure.')} />
</Modal.Columns>
<Modal.Columns hint={t('The key type and crypto type to use for this key. Be aware that different keys have different crypto requirements. You should be familiar with the type requirements for the different keys.')}>
<Dropdown
label={t('key type to set')}
onChange={setKeyType}
options={keyTypeOptRef.current}
value={keyType}
/>
<Dropdown
isDisabled={_cryptoOptions.length === 1}
label={t('crypto type to use')}
onChange={setCrypto}
options={_cryptoOptions}
value={crypto}
/>
</Modal.Columns>
<Modal.Columns hint={t('This pubic key is what will be visible in your queued keys list. It is generated based on the seed and the crypto used.')}>
<Input
isDisabled
label={t('generated public key')}
value={publicKey}
/>
</Modal.Columns>
</Modal.Content>
<Modal.Actions>
<Button
icon='sign-in-alt'
label={t('Submit key')}
onClick={_onSubmit}
/>
</Modal.Actions>
</Modal>
);
}

export default React.memo(InjectKeys);
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { DeriveBalancesAll } from '@polkadot/api-derive/types';
import type { AmountValidateState } from '../types.js';

import React, { useEffect, useState } from 'react';

import { MarkError, MarkWarning } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { BN, BN_TEN, BN_THOUSAND, BN_ZERO, formatBalance } from '@polkadot/util';

import { useTranslation } from '../../translate.js';

interface Props {
controllerId: string | null;
currentAmount?: BN | null;
isNominating?: boolean;
minNominated?: BN;
minNominatorBond?: BN;
minValidatorBond?: BN;
onError: (state: AmountValidateState | null) => void;
stashId: string | null;
value?: BN | null;
}

function formatExistential (value: BN): string {
let fmt = (
value
.mul(BN_THOUSAND)
.div(BN_TEN.pow(new BN(formatBalance.getDefaults().decimals)))
.toNumber() / 1000
).toFixed(3);

while (fmt.length !== 1 && ['.', '0'].includes(fmt[fmt.length - 1])) {
const isLast = fmt.endsWith('.');

fmt = fmt.substring(0, fmt.length - 1);

if (isLast) {
break;
}
}

return fmt;
}

function ValidateAmount ({ currentAmount, isNominating, minNominated, minNominatorBond, minValidatorBond, onError, stashId, value }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const stashBalance = useCall<DeriveBalancesAll>(api.derive.balances?.all, [stashId]);
const [{ error, warning }, setResult] = useState<AmountValidateState>({ error: null, warning: null });

useEffect((): void => {
if (stashBalance && value) {
// also used in bond extra, take check against total of current bonded and new
const check = value.add(currentAmount || BN_ZERO);
const existentialDeposit = api.consts.balances.existentialDeposit;
const maxBond = stashBalance.freeBalance.sub(existentialDeposit.divn(2));
let newError: string | null = null;
let newWarning: string | null = null;

if (check.gte(maxBond)) {
newWarning = t('The specified value is large and may not allow enough funds to pay future transaction fees.');
} else if (check.lt(existentialDeposit)) {
newError = t('The bonded amount is less than the minimum bond amount of {{existentialDeposit}}', {
replace: { existentialDeposit: formatExistential(existentialDeposit) }
});
} else if (isNominating) {
if (minNominatorBond && check.lt(minNominatorBond)) {
newError = t('The bonded amount is less than the minimum threshold of {{minBond}} for nominators', {
replace: { minBond: formatBalance(minNominatorBond) }
});
} else if (minNominated && check.lt(minNominated)) {
newWarning = t('The bonded amount is less than the current active minimum nominated amount of {{minNomination}} and depending on the network state, may not be selected to participate', {
replace: { minNomination: formatBalance(minNominated) }
});
}
} else {
if (minValidatorBond && check.lt(minValidatorBond)) {
newError = t('The bonded amount is less than the minimum threshold of {{minBond}} for validators', {
replace: { minBond: formatBalance(minValidatorBond) }
});
}
}

setResult((state): AmountValidateState => {
const error = state.error !== newError ? newError : state.error;
const warning = state.warning !== newWarning ? newWarning : state.warning;

onError(
(error || warning)
? { error, warning }
: null
);

return { error, warning };
});
}
}, [api, currentAmount, isNominating, minNominated, minNominatorBond, minValidatorBond, onError, stashBalance, t, value]);

if (error) {
return <MarkError content={error} />;
} else if (warning) {
return <MarkWarning content={warning} />;
}

return null;
}

export default React.memo(ValidateAmount);
Loading