Toss creaky signer Modal, revamp (#2933)

* Toss creaky signer Modal, revamp

* WIP

* WIP

* WIP

* WIP

* Basics seem working

* Adjust layout

* WIP

* Cleanups for unsigned

* Unsigned

* Start at signing-only

* Signing should be ok

* Signing operational

* Remove old modal

* Update packages/react-signer/src/Modal/TxSigned.tsx

* JS app integration

* Small styling adjustments

* Prelim proxy support

* Proxy on/off switch

* isProxyActive

* Multi + Proxy ordering (& testing)

* Proxy with multisig

* Remove debug logs

* Qr with 2 column layout

* Padding adjustments

* Cleanups

* Cleanup PaymentInfo

* Generic asset cleanup

* flatten

* Don't save latest values when filter is active
This commit is contained in:
Jaco Greeff 2020-06-14 18:59:22 +02:00 committed by GitHub
parent dcf2f80d2b
commit 601ae6da08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1185 additions and 1494 deletions

View File

@ -86,22 +86,6 @@ function Multisig ({ className = '', onClose, onStatusChange }: Props): React.Re
size='large'
>
<Modal.Content>
<Modal.Columns>
<Modal.Column>
<Input
autoFocus
className='full'
help={t<string>('Name given to this multisig. You can edit it at any later point in time.')}
isError={!isNameValid}
label={t<string>('name')}
onChange={_onChangeName}
placeholder={t<string>('multisig name')}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The name is for unique identification of the account in your owner lists.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<InputAddressMulti
@ -132,6 +116,22 @@ function Multisig ({ className = '', onClose, onStatusChange }: Props): React.Re
<p>{t<string>('The threshold for approval should be less or equal to the number of signatories for this multisig.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<Input
autoFocus
className='full'
help={t<string>('Name given to this multisig. You can edit it at any later point in time.')}
isError={!isNameValid}
label={t<string>('name')}
onChange={_onChangeName}
placeholder={t<string>('multisig name')}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The name is for unique identification of the account in your owner lists.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={onClose}>
<Button

View File

@ -3,7 +3,6 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
import { I18nProps } from '@polkadot/react-components/types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
@ -11,14 +10,13 @@ import styled from 'styled-components';
import { Button, InputAddress, InputBalance, TxButton, Dropdown } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { Available } from '@polkadot/react-query';
import Checks from '@polkadot/react-signer/Checks';
import { withMulti, withObservable } from '@polkadot/react-api/hoc';
import { BN_ZERO } from '@polkadot/util';
import assetRegistry, { AssetsSubjectInfo } from './assetsRegistry';
import translate from './translate';
import { useTranslation } from './translate';
interface Props extends I18nProps {
interface Props {
className?: string;
onClose: () => void;
recipientId?: string;
@ -31,12 +29,12 @@ interface Option {
value: string;
}
function Transfer ({ assets, className = '', onClose, recipientId: propRecipientId, senderId: propSenderId, t }: Props): React.ReactElement<Props> {
function Transfer ({ assets, className = '', onClose, recipientId: propRecipientId, senderId: propSenderId }: Props): React.ReactElement<Props> {
const { api } = useApi();
const { t } = useTranslation();
const [assetId, setAssetId] = useState('0');
const [amount, setAmount] = useState<BN | undefined>(BN_ZERO);
const [extrinsic, setExtrinsic] = useState<SubmittableExtrinsic | null>(null);
const [hasAvailable, setHasAvailable] = useState(true);
const [options, setOptions] = useState<Option[]>([]);
const [recipientId, setRecipientId] = useState(propRecipientId || null);
const [senderId, setSenderId] = useState(propSenderId || null);
@ -109,23 +107,15 @@ function Transfer ({ assets, className = '', onClose, recipientId: propRecipient
/>
<InputBalance
help={t<string>('Type the amount you want to transfer. Note that you can select the unit on the right e.g sending 1 milli is equivalent to sending 0.001.')}
isError={!hasAvailable}
label={t<string>('amount')}
onChange={setAmount}
/>
<Checks
accountId={senderId}
extrinsic={extrinsic}
isSendable
onChange={setHasAvailable}
/>
</div>
<Button.Group>
<TxButton
accountId={senderId}
extrinsic={extrinsic}
icon='send'
isDisabled={!hasAvailable}
isPrimary
label={t<string>('Make Transfer')}
onStart={onClose}
@ -156,6 +146,5 @@ export default withMulti(
flex-basis: 10rem;
}
`,
translate,
withObservable(assetRegistry.subject, { propName: 'assets' })
);

View File

@ -5,11 +5,17 @@
import React from 'react';
import styled from 'styled-components';
import Column from './Column';
interface Props {
children: React.ReactNode;
className?: string;
}
type ColumarType = React.ComponentType<Props> & {
Column: React.ComponentType<Props>;
};
function Columar ({ children, className = '' }: Props): React.ReactElement<Props> {
return (
<div className={`ui--Columnar ${className}`}>
@ -18,7 +24,7 @@ function Columar ({ children, className = '' }: Props): React.ReactElement<Props
);
}
export default React.memo(styled(Columar)`
const ColumarExp = React.memo(styled(Columar)`
display: flex;
flex-wrap: wrap;
@ -28,4 +34,8 @@ export default React.memo(styled(Columar)`
min-width: 50%;
}
}
`);
`) as unknown as ColumarType;
ColumarExp.Column = Column;
export default ColumarExp;

View File

@ -222,9 +222,9 @@ class InputAddress extends React.PureComponent<Props, State> {
}
private onChange = (address: string): void => {
const { onChange, type } = this.props;
const { filter, onChange, type } = this.props;
setLastValue(type, address);
!filter && setLastValue(type, address);
onChange && onChange(transformToAccountId(address));
}

View File

@ -0,0 +1,279 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { SubmittableExtrinsic } from '@polkadot/api/types';
import { QueueTx } from '@polkadot/react-components/Status/types';
import { Multisig, ProxyType } from '@polkadot/types/interfaces';
import { AddressProxy } from './types';
import React, { useEffect, useState } from 'react';
import { ApiPromise } from '@polkadot/api';
import { registry } from '@polkadot/react-api';
import { InputAddress, Modal, Toggle } from '@polkadot/react-components';
import { useApi, useIsMountedRef } from '@polkadot/react-hooks';
import { Option } from '@polkadot/types';
import { isFunction } from '@polkadot/util';
import { useTranslation } from './translate';
import Password from './Password';
import { extractExternal } from './util';
interface Props {
className?: string;
currentItem: QueueTx;
onChange: (address: AddressProxy) => void;
passwordError: string | null;
requestAddress: string;
}
interface MultiState {
address: string;
isMultiCall: boolean;
who: string[];
whoFilter: string[];
}
interface ProxyState {
address: string;
proxies: [string, ProxyType][];
proxiesFilter: string[];
}
function filterProxies (tx: SubmittableExtrinsic<'promise'>, proxies: [string, ProxyType][]): string[] {
const { method, section } = registry.findMetaCall(tx.callIndex);
return proxies
.filter(([, proxy]): boolean => {
switch (proxy.toString()) {
case 'Any':
return true;
case 'Governance':
return ['council', 'democracy', 'elections', 'electionsPhragmen', 'society', 'technicalCommittee', 'treasury'].includes(section);
case 'NonTransfer':
return !(section === 'balances' || (section === 'indices' && method === 'transfer') || (section === 'vesting' && method === 'vestedTransfer'));
case 'Staking':
// Call::Utility(utility::Call::batch(..)) | Call::Utility(utility::Call::as_limited_sub(..))
return section === 'staking' || section === 'utility';
case 'SudoBalances':
// return Sudo(sudo::Call::sudo(ref x)) => matches!(x.as_ref(), &Call::Balances(..)), Call::Utility(utility::Call::batch(..))
return section === 'sudo' || section === 'utility';
default:
return false;
}
})
.map(([address]) => address);
}
async function queryForMultisig (api: ApiPromise, requestAddress: string, proxyAddress: string | null, tx?: SubmittableExtrinsic<'promise'>): Promise<MultiState | null> {
const multiModule = api.tx.multisig ? 'multisig' : 'utility';
if (tx && isFunction(api.query[multiModule]?.multisigs)) {
const address = proxyAddress || requestAddress;
const { threshold, who } = extractExternal(address);
const hash = (proxyAddress ? api.tx.proxy.proxy(requestAddress, null, tx) : tx).method.hash;
const optMulti = await api.query[multiModule].multisigs<Option<Multisig>>(address, hash);
const multi = optMulti.unwrapOr(null);
return multi
? {
address,
isMultiCall: ((multi.approvals.length + 1) >= threshold),
who,
whoFilter: who.filter((w) => !multi.approvals.some((a) => a.eq(w)))
}
: {
address,
isMultiCall: false,
who,
whoFilter: who
};
}
return null;
}
async function queryForProxy (api: ApiPromise, address: string, tx: SubmittableExtrinsic<'promise'>): Promise<ProxyState | null> {
if (isFunction(api.query.proxy?.proxies)) {
const [_proxies] = await api.query.proxy.proxies(address);
if (_proxies.length) {
const proxies = _proxies.map(([accountId, type]): [string, ProxyType] => [accountId.toString(), type]);
const proxiesFilter = filterProxies(tx, proxies);
return proxiesFilter.length
? { address, proxies, proxiesFilter }
: null;
}
}
return null;
}
function Address ({ currentItem, onChange, passwordError, requestAddress }: Props): React.ReactElement<Props> {
const { api } = useApi();
const mountedRef = useIsMountedRef();
const { t } = useTranslation();
const [multiAddress, setMultiAddress] = useState<string | null>(null);
const [proxyAddress, setProxyAddress] = useState<string | null>(null);
const [flags, setFlags] = useState(extractExternal(requestAddress));
const [isMultiCall, setIsMultiCall] = useState(false);
const [isProxyActive, setIsProxyActive] = useState(true);
const [multiInfo, setMultInfo] = useState<MultiState | null>(null);
const [proxyInfo, setProxyInfo] = useState<ProxyState | null>(null);
const [signAddress, setSignAddress] = useState<string | null>(requestAddress);
const [signPassword, setSignPassword] = useState('');
useEffect((): void => {
setFlags(extractExternal(signAddress));
}, [signAddress]);
// proxy for requestor
useEffect((): void => {
if (currentItem.extrinsic) {
queryForProxy(api, requestAddress, currentItem.extrinsic)
.then((info) => mountedRef.current && setProxyInfo(info))
.catch(console.error);
} else {
setProxyInfo(null);
}
}, [api, currentItem, mountedRef, requestAddress]);
useEffect((): void => {
!proxyInfo && setProxyAddress(null);
}, [proxyInfo]);
// multisig
useEffect((): void => {
if (extractExternal(proxyAddress || requestAddress).isMultisig) {
queryForMultisig(api, requestAddress, proxyAddress, currentItem.extrinsic)
.then((info): void => {
if (mountedRef.current) {
setMultInfo(info);
setIsMultiCall(info?.isMultiCall || false);
}
})
.catch(console.error);
} else {
setMultInfo(null);
}
}, [proxyAddress, api, currentItem, mountedRef, requestAddress]);
// address
useEffect((): void => {
setSignAddress(
(multiInfo && multiAddress) ||
(isProxyActive && proxyInfo && proxyAddress) ||
requestAddress
);
}, [multiAddress, proxyAddress, isProxyActive, multiInfo, proxyInfo, requestAddress]);
useEffect((): void => {
onChange({
isMultiCall: isMultiCall,
multiRoot: multiInfo ? multiInfo.address : null,
proxyRoot: (proxyInfo && isProxyActive) ? proxyInfo.address : null,
signAddress,
signPassword
});
}, [isProxyActive, isMultiCall, multiAddress, multiInfo, onChange, proxyAddress, proxyInfo, signAddress, signPassword]);
return (
<>
<Modal.Columns>
<Modal.Column>
<InputAddress
className='full'
defaultValue={requestAddress}
isDisabled
isInput
label={t('sending from my account')}
withLabel
/>
</Modal.Column>
<Modal.Column>
<p>{t('The sending account that will be used to send this transaction. Any applicable fees will be paid by this account.')}</p>
</Modal.Column>
</Modal.Columns>
{proxyInfo && isProxyActive && (
<Modal.Columns>
<Modal.Column>
<InputAddress
filter={proxyInfo.proxiesFilter}
help={t('The proxy to be used for this transaction.')}
label={t('proxy account')}
onChange={setProxyAddress}
type='account'
/>
</Modal.Column>
<Modal.Column>
<p>{t('The proxy is one of the allowed proxies on the account, as set and filtered by the transaction type.')}</p>
</Modal.Column>
</Modal.Columns>
)}
{multiInfo && (
<Modal.Columns>
<Modal.Column>
<InputAddress
filter={multiInfo.whoFilter}
help={t('The multisig signatory for this transaction.')}
label={t('multisig signatory')}
onChange={setMultiAddress}
type='account'
/>
</Modal.Column>
<Modal.Column>
<p>{t('The signatory is one of the allowed accounts on the multisig, making a recorded approval for the transaction.')}</p>
</Modal.Column>
</Modal.Columns>
)}
{signAddress && !currentItem.isUnsigned && flags.isUnlockable && (
<Password
address={signAddress}
error={passwordError}
onChange={setSignPassword}
/>
)}
{proxyInfo && (
<Modal.Columns>
<Modal.Column>
<Toggle
className='tipToggle'
label={
isProxyActive
? t<string>('Use a proxy for this call')
: t<string>("Don't use a proxy for this call")
}
onChange={setIsProxyActive}
value={isProxyActive}
/>
</Modal.Column>
<Modal.Column>
<p>{t('This could either be an approval for the hash or with full call details. The call as last approval triggers execution.')}</p>
</Modal.Column>
</Modal.Columns>
)}
{multiInfo && (
<Modal.Columns>
<Modal.Column>
<Toggle
className='tipToggle'
label={
isMultiCall
? t<string>('Multisig message with call (for final approval)')
: t<string>('Multisig approval with hash (non-final approval)')
}
onChange={setIsMultiCall}
value={isMultiCall}
/>
</Modal.Column>
<Modal.Column>
<p>{t('This could either be an approval for the hash or with full call details. The call as last approval triggers execution.')}</p>
</Modal.Column>
</Modal.Columns>
)}
</>
);
}
export default React.memo(Address);

View File

@ -1,50 +0,0 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveContractFees } from '@polkadot/api-derive/types';
import { ExtraFees as State } from './types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import { Compact, UInt } from '@polkadot/types';
import { BN_ZERO } from '@polkadot/util';
interface Props {
endowment: BN | Compact<UInt>;
fees: DeriveContractFees;
onChange: (fees: State) => void;
}
function ContractCall ({ endowment, fees, onChange }: Props): React.ReactElement<Props> | null {
const [, setState] = useState<State>({
extraAmount: BN_ZERO,
extraFees: BN_ZERO,
extraWarn: false
});
useEffect((): void => {
const extraFees = new BN(fees.callBaseFee);
const extraAmount = endowment instanceof Compact
? endowment.toBn()
: new BN(endowment || 0);
const update = {
extraAmount,
extraFees,
extraWarn: false
};
setState((state): State => {
if (!update.extraAmount.eq(state.extraAmount) || !update.extraFees.eq(state.extraFees)) {
onChange(update);
}
return update;
});
}, [endowment, fees, onChange]);
return null;
}
export default React.memo(ContractCall);

View File

@ -1,50 +0,0 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveContractFees } from '@polkadot/api-derive/types';
import { ExtraFees as State } from './types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import { Compact, UInt } from '@polkadot/types';
import { BN_ZERO } from '@polkadot/util';
interface Props {
endowment: BN | Compact<UInt>;
fees: DeriveContractFees;
onChange: (fees: State) => void;
}
function ContractDeploy ({ endowment, fees, onChange }: Props): React.ReactElement<Props> | null {
const [, setState] = useState<State>({
extraAmount: BN_ZERO,
extraFees: BN_ZERO,
extraWarn: false
});
useEffect((): void => {
const extraFees = fees.contractFee;
const extraAmount = endowment instanceof Compact
? endowment.toBn()
: new BN(endowment || 0);
const update = {
extraAmount,
extraFees,
extraWarn: false
};
setState((state): State => {
if (!update.extraAmount.eq(state.extraAmount) || !update.extraFees.eq(state.extraFees)) {
onChange(update);
}
return update;
});
}, [endowment, fees, onChange]);
return null;
}
export default React.memo(ContractDeploy);

View File

@ -1,87 +0,0 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveFees } from '@polkadot/api-derive/types';
import { ExtraFees } from './types';
import BN from 'bn.js';
import React, { useState, useEffect } from 'react';
import { Compact, UInt } from '@polkadot/types';
import { Icon } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { formatBalance } from '@polkadot/util';
import { useTranslation } from '../translate';
interface Props {
deposit: BN | Compact<UInt>;
fees: DeriveFees;
onChange: (fees: ExtraFees) => void;
}
interface State extends ExtraFees {
isBelowMinimum: boolean;
}
const ZERO = new BN(0);
function Proposal ({ deposit, onChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const minDeposit = api.consts.democracy.minimumDeposit;
const [{ extraAmount, isBelowMinimum }, setState] = useState<State>({
extraAmount: ZERO,
extraFees: ZERO,
extraWarn: false,
isBelowMinimum: false
});
useEffect((): void => {
if (minDeposit) {
const extraAmount = deposit instanceof Compact
? deposit.toBn()
: deposit;
const isBelowMinimum = extraAmount.lt(minDeposit);
const update = {
extraAmount,
extraFees: ZERO,
extraWarn: isBelowMinimum
};
onChange(update);
setState({
...update,
isBelowMinimum
});
}
}, [deposit, minDeposit, onChange]);
return (
<>
{isBelowMinimum && (
<div>
<Icon name='warning sign' />
{t<string>('The deposit is below the {{minimum}} minimum required for the proposal to be evaluated', {
replace: {
minimum: formatBalance(minDeposit, { forceUnit: '-' })
}
})}
</div>
)}
{!extraAmount.isZero() && (
<div>
<Icon name='arrow right' />
{t<string>('The deposit of {{deposit}} will be reserved until the proposal is completed', {
replace: {
deposit: formatBalance(extraAmount, { forceUnit: '-' })
}
})}
</div>
)}
</>
);
}
export default React.memo(Proposal);

View File

@ -1,92 +0,0 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveBalancesAll, DeriveFees } from '@polkadot/api-derive/types';
import { AccountId } from '@polkadot/types/interfaces';
import { ExtraFees } from './types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import { Compact, UInt } from '@polkadot/types';
import { Icon } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { BN_ZERO, formatBalance } from '@polkadot/util';
import { useTranslation } from '../translate';
interface Props {
amount: BN | Compact<UInt>;
fees: DeriveFees;
recipientId: string | AccountId;
onChange: (fees: ExtraFees) => void;
}
interface State extends ExtraFees {
isCreation: boolean;
isNoEffect: boolean;
}
function Transfer ({ amount, fees, onChange, recipientId }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const allBalances = useCall<DeriveBalancesAll>(api.derive.balances.all, [recipientId]);
const [{ isCreation, isNoEffect }, setState] = useState<State>({
extraAmount: BN_ZERO,
extraFees: BN_ZERO,
extraWarn: false,
isCreation: false,
isNoEffect: false
});
useEffect((): void => {
if (allBalances) {
let extraFees = new BN(fees.transferFee);
if (allBalances.votingBalance.isZero()) {
extraFees = extraFees.add(fees.creationFee);
}
const extraAmount = amount instanceof Compact ? amount.unwrap() : new BN(amount);
const isCreation = allBalances.votingBalance.isZero() && fees.creationFee.gtn(0);
const isNoEffect = extraAmount.add(allBalances.votingBalance).lt(fees.existentialDeposit);
const extraWarn = isCreation || isNoEffect;
const update = { extraAmount, extraFees, extraWarn };
onChange(update);
setState({
...update,
isCreation,
isNoEffect
});
}
}, [amount, allBalances, fees, onChange]);
return (
<>
{isNoEffect && (
<div>
<Icon name='warning sign' />
{t<string>('The final recipient balance is less or equal to {{existentialDeposit}} (the existential amount) and will not be reflected', {
replace: {
existentialDeposit: formatBalance(fees.existentialDeposit)
}
})}
</div>
)}
{isCreation && (
<div>
<Icon name='warning sign' />
{t<string>('A fee of {{creationFee}} will be deducted from the sender since the destination account does not exist', {
replace: {
creationFee: formatBalance(fees.creationFee)
}
})}
</div>
)}
</>
);
}
export default React.memo(Transfer);

View File

@ -1,60 +0,0 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveFees, DeriveBalancesAll, DeriveContractFees } from '@polkadot/api-derive/types';
import { registry } from '@polkadot/react-api';
import { createType } from '@polkadot/types';
import { BN_ZERO } from '@polkadot/util';
const ZERO_BALANCE: DeriveBalancesAll = {
accountId: createType(registry, 'AccountId'),
accountNonce: createType(registry, 'Index'),
availableBalance: createType(registry, 'Balance'),
freeBalance: createType(registry, 'Balance'),
frozenFee: createType(registry, 'Balance'),
frozenMisc: createType(registry, 'Balance'),
isVesting: false,
lockedBalance: createType(registry, 'Balance'),
lockedBreakdown: [],
reservedBalance: createType(registry, 'Balance'),
vestedBalance: createType(registry, 'Balance'),
vestingTotal: createType(registry, 'Balance'),
votingBalance: createType(registry, 'Balance')
};
const ZERO_FEES_BALANCES: DeriveFees = {
creationFee: createType(registry, 'Balance'),
existentialDeposit: createType(registry, 'Balance'),
transactionBaseFee: createType(registry, 'Balance'),
transactionByteFee: createType(registry, 'Balance'),
transferFee: createType(registry, 'Balance')
};
const ZERO_FEES = ZERO_FEES_BALANCES;
const ZERO_FEES_CONTRACT: DeriveContractFees = {
callBaseFee: BN_ZERO,
contractFee: BN_ZERO,
creationFee: BN_ZERO,
rentByteFee: BN_ZERO,
rentDepositOffset: BN_ZERO,
surchargeReward: BN_ZERO,
tombstoneDeposit: BN_ZERO,
transactionBaseFee: BN_ZERO,
transactionByteFee: BN_ZERO,
transferFee: BN_ZERO
};
const MAX_SIZE_MB = 10;
const MAX_SIZE_BYTES = MAX_SIZE_MB * 1024 * 1024;
export {
ZERO_BALANCE,
ZERO_FEES,
ZERO_FEES_BALANCES,
ZERO_FEES_CONTRACT,
MAX_SIZE_BYTES,
MAX_SIZE_MB
};

View File

@ -1,11 +0,0 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import BN from 'bn.js';
export interface ExtraFees {
extraAmount: BN;
extraFees: BN;
extraWarn: boolean;
}

View File

@ -8,12 +8,14 @@ import { SignerPayloadJSON } from '@polkadot/types/types';
import { getLedger, registry } from '@polkadot/react-api';
import { createType } from '@polkadot/types';
let id = 0;
export class LedgerSigner implements Signer {
public async signPayload (payload: SignerPayloadJSON): Promise<SignerResult> {
const raw = createType(registry, 'ExtrinsicPayload', payload, { version: payload.version });
const { signature } = await getLedger().sign(raw.toU8a(true));
return { id: 0, signature };
return { id: ++id, signature };
}
}

View File

@ -1,957 +0,0 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { SignerOptions, SignerResult, Signer as ApiSigner } from '@polkadot/api/types';
import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
import { ApiProps } from '@polkadot/react-api/types';
import { I18nProps, BareProps } from '@polkadot/react-components/types';
import { KeyringPair } from '@polkadot/keyring/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { QueueTx, QueueTxMessageSetStatus, QueueTxResult, QueueTxStatus } from '@polkadot/react-components/Status/types';
import { Multisig, Timepoint } from '@polkadot/types/interfaces';
import { DefinitionRpcExt, SignerPayloadJSON } from '@polkadot/types/types';
import BN from 'bn.js';
import React from 'react';
import { SubmittableResult } from '@polkadot/api';
import { web3FromSource } from '@polkadot/extension-dapp';
import { Option, createType } from '@polkadot/types';
import { Button, InputBalance, Modal, Toggle, Output, ErrorBoundary, InputNumber, InputAddress } from '@polkadot/react-components';
import { registry } from '@polkadot/react-api';
import { withApi, withMulti, withObservable } from '@polkadot/react-api/hoc';
import keyring from '@polkadot/ui-keyring';
import { BN_ZERO, assert, isFunction } from '@polkadot/util';
import { blake2AsU8a } from '@polkadot/util-crypto';
import { format } from '@polkadot/util/logger';
import ledgerSigner from './LedgerSigner';
import Transaction from './Transaction';
import Qr from './Qr';
import Unlock from './Unlock';
import translate from './translate';
interface BaseProps extends BareProps {
queue: QueueTx[];
queueSetTxStatus: QueueTxMessageSetStatus;
}
interface Props extends I18nProps, ApiProps, BaseProps {
allAccounts?: SubjectInfo;
}
interface State {
accountNonce?: BN;
blocks: BN;
currentItem?: QueueTx;
isQrScanning: boolean;
isQrVisible: boolean;
isRenderError: boolean;
isSendable: boolean;
isSubmit: boolean;
multiCall: boolean;
nonce?: BN;
password: string;
qrAddress: string;
qrIsHashed: boolean;
qrPayload: Uint8Array;
qrResolve?: (result: SignerResult) => void;
qrReject?: (error: Error) => void;
signatory?: string | null;
showTip: boolean;
signedTx?: string;
tip?: BN;
unlockError?: string | null;
whoFiltered: string[] | null;
}
interface AccountFlags {
hardwareType?: string;
isExternal: boolean;
isHardware: boolean;
isMultisig: boolean;
threshold: number;
who: string[];
}
let qrId = 0;
function extractExternal (accountId?: string | null): AccountFlags {
if (!accountId) {
return { isExternal: false, isHardware: false, isMultisig: false, threshold: 0, who: [] };
}
let publicKey;
try {
publicKey = keyring.decodeAddress(accountId);
} catch (error) {
console.error(error);
return { isExternal: false, isHardware: false, isMultisig: false, threshold: 0, who: [] };
}
const pair = keyring.getPair(publicKey);
return {
hardwareType: pair.meta.hardwareType as string,
isExternal: !!pair.meta.isExternal,
isHardware: !!pair.meta.isHardware,
isMultisig: !!pair.meta.isMultisig,
threshold: (pair.meta.threshold as number) || 0,
who: ((pair.meta.who as string[]) || []).map((w) => keyring.encodeAddress(keyring.decodeAddress(w)))
};
}
// eslint-disable-next-line @typescript-eslint/require-await
async function makeExtrinsicSignature (
payload: SignerPayloadJSON,
{ id, signerCb }: QueueTx,
pair: KeyringPair
): Promise<void> {
console.log('makeExtrinsicSignature: payload ::', JSON.stringify(payload));
const result = createType(registry, 'ExtrinsicPayload', payload, { version: payload.version }).sign(pair);
if (isFunction(signerCb)) {
signerCb(id, { id, ...result });
}
}
const initialState: State = {
accountNonce: undefined,
blocks: new BN(50),
isQrScanning: false,
isQrVisible: false,
isRenderError: false,
isSendable: false,
isSubmit: true,
multiCall: false,
nonce: undefined,
password: '',
qrAddress: '',
qrIsHashed: false,
qrPayload: new Uint8Array(),
showTip: false,
signedTx: '',
unlockError: null,
whoFiltered: null
};
class Signer extends React.PureComponent<Props, State> {
public state: State = initialState;
public static getDerivedStateFromProps ({ allAccounts, queue }: Props, { currentItem, multiCall, password, unlockError }: State): Partial<State> {
const nextItem = queue.find(({ status }) => ['queued', 'qr'].includes(status));
const isSame =
!!nextItem &&
!!currentItem &&
((!nextItem.accountId && !currentItem.accountId) ||
(nextItem.accountId && nextItem.accountId.toString()) ===
(currentItem.accountId && currentItem.accountId.toString()));
let isSendable = !!nextItem && !!nextItem.isUnsigned;
if (!isSendable && nextItem && nextItem.accountId && allAccounts) {
try {
const pair = keyring.getPair(nextItem.accountId);
isSendable = !!pair && !!allAccounts[nextItem.accountId];
} catch (error) {
// swallow
}
}
return {
currentItem: nextItem,
isSendable,
...(isSame
? { multiCall, password, unlockError }
: { multiCall: false, password: '', signatory: null, unlockError: null, whoFiltered: null }
)
};
}
public async componentDidUpdate (): Promise<void> {
const { accountNonce, currentItem, isSendable, isSubmit, whoFiltered } = this.state;
if (currentItem && currentItem.status === 'queued' && !(currentItem.extrinsic || currentItem.payload)) {
return this.sendRpc(currentItem);
}
if (currentItem?.accountId) {
if (!isSubmit && accountNonce == null) {
this.updateNonce().catch(console.error);
}
if (isSendable && !whoFiltered) {
const { isMultisig } = extractExternal(currentItem.accountId);
if (isMultisig) {
this.checkMultisig().catch(console.error);
}
}
}
}
public render (): React.ReactNode {
const { className = '', t } = this.props;
const { currentItem } = this.state;
if (!currentItem) {
return null;
}
return (
<Modal
className={`ui--signer-Signer ${className}`}
header={t<string>('Authorize transaction')}
size='large'
>
<ErrorBoundary onError={this.onRenderError}>{this.renderContent()}</ErrorBoundary>
{this.renderButtons()}
</Modal>
);
}
private renderButtons (): React.ReactNode {
const { t } = this.props;
const { currentItem, isQrScanning, isQrVisible, isRenderError, isSendable, isSubmit, signatory, signedTx } = this.state;
if (!currentItem) {
return null;
}
const { hardwareType, isExternal, isHardware, isMultisig } = extractExternal(currentItem.accountId);
return (
<Modal.Actions
cancelLabel={signedTx ? t<string>('Close') : undefined}
onCancel={
isQrVisible
? this.onCancelQr
: signedTx
? this.onCancelSign
: this.onCancel
}
withOr={!signedTx && !isQrVisible}
>
{!isRenderError && (!isQrVisible || !isQrScanning) && !signedTx && (
<>
<Button
className='ui--signer-Signer-Submit'
icon={isQrVisible ? 'qrcode' : currentItem.isUnsigned ? 'sign-in' : (isExternal && !isMultisig) ? 'qrcode' : 'sign-in'}
isDisabled={!isSendable || (isMultisig && !signatory)}
isPrimary
label={
isQrVisible
? t<string>('Scan Signature Qr')
: currentItem.isUnsigned
? t<string>('Submit (no signature)')
: isHardware
? t<string>('Sign via {{hardwareType}}', { replace: { hardwareType: hardwareType || 'hardware' } })
: isExternal
? isMultisig
? t<string>('Sign for multisig')
: t<string>('Sign via Qr')
: isSubmit
? t<string>('Sign and Submit')
: t<string>('Sign (no submission)')
}
onClick={isQrVisible ? this.activateQrScanning : this.onSend}
tabIndex={2}
/>
{!currentItem.isUnsigned && this.renderSignToggle()}
</>
)}
</Modal.Actions>
);
}
private renderContent (): React.ReactNode {
const { api, t } = this.props;
const { currentItem, isQrScanning, isQrVisible, isSendable, isSubmit, qrAddress, qrIsHashed, qrPayload, tip } = this.state;
if (!currentItem) {
return null;
}
return (
<Transaction
hideDetails={isQrVisible}
isSendable={isSendable}
onError={this.onRenderError}
tip={tip}
value={currentItem}
>
{isQrVisible
? (
<Modal.Columns>
<Modal.Column>
<Qr
address={qrAddress}
genesisHash={api.genesisHash}
isHashed={qrIsHashed}
isScanning={isQrScanning}
onSignature={this.addQrSignature}
payload={qrPayload}
/>
</Modal.Column>
<Modal.Column>
{isQrScanning
? <p>{t<string>('Present the QR code containing the signature to the UI. Once scanned it will be submitted for on-chain processing and execution.')}</p>
: <p>{t<string>('Scan the QR code with your QR scanner. Once approved, you will be required to present the signed QR back to the UI for submission.')}</p>
}
</Modal.Column>
</Modal.Columns>
)
: (
<>
{this.renderTip()}
{this.renderSignatory()}
{this.renderUnlock()}
{!isSubmit && this.renderSignFields()}
</>
)}
</Transaction>
);
}
private renderSignatory (): React.ReactNode {
const { t } = this.props;
const { currentItem, multiCall, whoFiltered } = this.state;
const { isMultisig, who } = currentItem
? extractExternal(currentItem.accountId)
: { isMultisig: false, who: [] };
if (!currentItem || !isMultisig) {
return null;
}
return (
<>
<Modal.Columns>
<Modal.Column>
<InputAddress
filter={whoFiltered || who}
help={t<string>('The multisig signatory for this transaction.')}
label={t<string>('signatory')}
onChange={this.onChangeSignatory}
type='account'
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The signatory is one of the allowed accounts on the multisig, making a recorded approval for the transaction.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<Toggle
className='tipToggle'
label={
multiCall
? t<string>('Multisig message with call (for final approval)')
: t<string>('Multisig approval with hash (non-final approval)')
}
onChange={this.onToggleMultiCall}
value={multiCall}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('This could either be an approval for the hash or with full call details. The call as last approval triggers execution.')}</p>
</Modal.Column>
</Modal.Columns>
</>
);
}
private renderTip (): React.ReactNode {
const { t } = this.props;
const { currentItem, isSendable, showTip, signedTx } = this.state;
if (!isSendable || !currentItem || currentItem.isUnsigned) {
return null;
}
return (
<Modal.Columns>
<Modal.Column>
<Toggle
className='tipToggle'
isDisabled={!!signedTx}
label={
showTip
? t<string>('Include an optional tip for faster processing')
: t<string>('Do not include a tip for the block author')
}
onChange={this.onShowTip}
value={showTip}
/>
{showTip && (
<InputBalance
defaultValue={BN_ZERO}
help={t<string>('Add a tip to this extrinsic, paying the block author for greater priority')}
isDisabled={!!signedTx}
isZeroable
label={t<string>('Tip (optional)')}
onChange={this.onChangeTip}
/>
)}
</Modal.Column>
<Modal.Column>
<p>{t<string>('Adding an optional tip to the transaction could allow for higher priority, especially when the chain is busy.')}</p>
</Modal.Column>
</Modal.Columns>
);
}
private renderSignToggle (): React.ReactNode {
const { t } = this.props;
const { isQrScanning, isQrVisible, isSubmit } = this.state;
return (
<Toggle
className='signToggle'
isDisabled={isQrVisible || isQrScanning}
label={
isSubmit
? t<string>('Sign and Submit')
: t<string>('Sign (no submission)')
}
onChange={this.onToggleSign}
value={isSubmit}
/>
);
}
private renderSignFields (): React.ReactNode {
const { t } = this.props;
const { accountNonce, blocks, isSubmit, signedTx } = this.state;
if (isSubmit || accountNonce == null) {
return null;
}
return (
<>
<Modal.Columns>
<Modal.Column>
<InputNumber
isDisabled={!!signedTx}
isZeroable
label={t<string>('Nonce')}
labelExtra={t<string>('Current account nonce: {{accountNonce}}', { replace: { accountNonce } })}
onChange={this.onChangeNonce}
value={accountNonce}
/>
<InputNumber
isDisabled={!!signedTx}
isZeroable
label={t<string>('Lifetime (# of blocks)')}
labelExtra={t<string>('Set to 0 to make transaction immortal')}
onChange={this.onChangeBlocks}
value={blocks}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('Override any applicable values for the specific signed output. These will be used to construct and display the signed transaction.')}</p>
</Modal.Column>
</Modal.Columns>
{!!signedTx && (
<Output
label={t<string>('Signed transaction')}
value={signedTx}
withCopy
/>
)}
</>
);
}
private onRenderError = (): void => {
this.setState({ isRenderError: true });
};
private onShowTip = (showTip: boolean): void => {
this.setState({ showTip });
};
private onToggleMultiCall = (multiCall: boolean): void => {
this.setState({ multiCall });
}
private onToggleSign = (isSubmit: boolean): void => {
this.setState({ isSubmit });
}
private onChangeNonce = (value?: BN): void => {
this.setState({ nonce: value || BN_ZERO });
}
private onChangeBlocks = (value?: BN): void => {
this.setState({ blocks: value || BN_ZERO });
}
private onChangeSignatory = (signatory?: string | null): void => {
this.setState({ signatory });
}
private renderUnlock (): React.ReactNode {
const { currentItem, isSendable, password, signatory, unlockError } = this.state;
const { isExternal, isMultisig } = currentItem
? extractExternal(currentItem.accountId)
: { isExternal: false, isMultisig: false };
if (!isSendable || !currentItem || currentItem.isUnsigned || (isExternal && !isMultisig)) {
return null;
}
if (isMultisig) {
const { isExternal, isHardware } = extractExternal(signatory);
if (isExternal || isHardware) {
return null;
}
}
return (
<Unlock
autoFocus
error={unlockError || undefined}
onChange={this.onChangePassword}
onEnter={this.onSend}
password={password}
tabIndex={1}
value={isMultisig ? signatory : currentItem.accountId}
/>
);
}
private unlockAccount (accountId: string, password?: string): string | null {
let publicKey;
try {
publicKey = keyring.decodeAddress(accountId);
} catch (error) {
console.error(error);
return 'unable to decode address';
}
const pair = keyring.getPair(publicKey);
if (!pair.isLocked || pair.meta.isInjected || (pair.meta.isExternal && !pair.meta.isMultisig)) {
return null;
}
try {
pair.decodePkcs8(password);
} catch (error) {
console.error(error);
return (error as Error).message;
}
return null;
}
private onChangePassword = (password: string): void => {
this.setState({
password,
unlockError: null
});
};
private onChangeTip = (tip?: BN): void => {
this.setState({ tip });
};
private onCancelQr = (): void => {
this.setState({ isQrScanning: false, isQrVisible: false }, (): void => {
const { qrReject } = this.state;
qrReject && qrReject(new Error('cancelled'));
this.onCancel();
});
};
private onCancelSign = (): void => {
const { queueSetTxStatus } = this.props;
const { currentItem } = this.state;
const { id, txSuccessCb } = currentItem as QueueTx;
queueSetTxStatus(id, 'completed');
if (isFunction(txSuccessCb)) {
txSuccessCb({} as any);
}
this.setState(initialState);
}
private onCancel = (): void => {
const { queueSetTxStatus } = this.props;
const { currentItem } = this.state;
// This should never be executed
if (!currentItem) {
return;
}
const { id, signerCb, txFailedCb } = currentItem;
queueSetTxStatus(id, 'cancelled');
if (isFunction(signerCb)) {
signerCb(id, null);
}
if (isFunction(txFailedCb)) {
txFailedCb(null);
}
this.setState(initialState);
};
private onSend = async (): Promise<void> => {
const { currentItem, password } = this.state;
// This should never be executed
if (!currentItem) {
return;
}
return this.sendExtrinsic(currentItem, password);
};
private updateNonce = async (): Promise<void> => {
const { currentItem } = this.state;
let accountNonce: BN | undefined;
if (currentItem?.accountId) {
accountNonce = await this.props.api.rpc.account.nextIndex(currentItem.accountId);
} else {
accountNonce = undefined;
}
this.setState({ accountNonce, nonce: accountNonce });
};
private checkMultisig = async (): Promise<void> => {
const { api } = this.props;
const { currentItem } = this.state;
if (currentItem?.accountId && currentItem?.extrinsic) {
const multiModule = api.tx.multisig ? 'multisig' : 'utility';
const { threshold, who } = extractExternal(currentItem.accountId);
const optMulti = await api.query[multiModule].multisigs<Option<Multisig>>(currentItem.accountId, currentItem.extrinsic.method.hash);
const multi = optMulti.unwrapOr(null);
if (multi) {
this.setState({
multiCall: ((multi.approvals.length + 1) >= threshold),
whoFiltered: who.filter((w) => !multi.approvals.some((a) => a.eq(w)))
});
}
}
}
private signQrPayload = (payload: SignerPayloadJSON): Promise<SignerResult> => {
return new Promise((resolve, reject): void => {
// limit size of the transaction
const qrIsHashed = (payload.method.length > 5000);
const wrapper = registry.createType('ExtrinsicPayload', payload, { version: payload.version });
const qrPayload = qrIsHashed
? blake2AsU8a(wrapper.toU8a(true))
: wrapper.toU8a();
this.setState({
isQrVisible: true,
qrAddress: payload.address,
qrIsHashed,
qrPayload,
qrReject: reject,
qrResolve: resolve
});
});
};
private addQrSignature = ({ signature }: { signature: string }): void => {
this.setState(
({ qrResolve }: State): Pick<State, never> => {
qrResolve &&
qrResolve({
id: ++qrId,
signature
});
return {
isQrScanning: false,
isQrVisible: false
};
}
);
};
private activateQrScanning = (): void => {
this.setState({ isQrScanning: true });
};
private sendRpc = async ({ id, rpc, values = [] }: QueueTx): Promise<void> => {
if (!rpc) {
return;
}
const { queueSetTxStatus } = this.props;
queueSetTxStatus(id, 'sending');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { error, result, status } = await this.submitRpc(rpc, values);
queueSetTxStatus(id, status, result, error);
};
private async sendExtrinsic (queueTx: QueueTx, password?: string): Promise<void> {
const { api, queueSetTxStatus } = this.props;
const { isSubmit, multiCall, showTip, signatory, tip } = this.state;
const { accountId, extrinsic, id, isUnsigned, payload } = queueTx;
if (!isUnsigned) {
assert(accountId, 'Expected an accountId with signed transactions');
const unlockError = this.unlockAccount((signatory || accountId), password);
if (unlockError) {
this.setState({ unlockError });
return;
}
}
if (payload) {
queueSetTxStatus(id, 'completed');
return makeExtrinsicSignature(
{
...payload,
...(showTip && tip && !payload.tip ? { tip: tip.toString() } : {})
},
queueTx,
keyring.getPair((signatory || accountId) as string)
);
}
const submittable = extrinsic as SubmittableExtrinsic;
assert(submittable, 'Expected an extrinsic to be supplied to sendExtrinsic');
if (isUnsigned) {
return this.makeExtrinsicCall(submittable, queueTx, submittable.send.bind(submittable));
}
const basePair = keyring.getPair(accountId as string);
let pair = basePair;
let tx = submittable;
if (basePair.meta.isMultisig) {
const multiModule = api.tx.multisig ? 'multisig' : 'utility';
const others = (basePair.meta.who as string[])
.map((w) => keyring.encodeAddress(keyring.decodeAddress(w)))
.filter((w) => w !== signatory);
const info = await api.query[multiModule].multisigs<Option<Multisig>>(accountId as string, submittable.method.hash);
let timepoint: Timepoint | null = null;
if (info.isSome) {
timepoint = info.unwrap().when;
}
pair = keyring.getPair(signatory as string);
tx = multiCall
? api.tx[multiModule].asMulti.meta.args.length === 5
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
? api.tx[multiModule].asMulti(basePair.meta.threshold as number, others, timepoint, submittable.method, false)
: api.tx[multiModule].asMulti(basePair.meta.threshold as number, others, timepoint, submittable.method)
: api.tx[multiModule].approveAsMulti(basePair.meta.threshold as number, others, timepoint, submittable.method.hash);
}
console.log('sendExtrinsic::', JSON.stringify(tx.method.toHuman()));
return isSubmit
? this.makeExtrinsicCall(tx, queueTx, tx.signAndSend.bind(tx), pair)
: this.makeSignedTransaction(tx, queueTx, pair);
}
private async submitRpc ({ method, section }: DefinitionRpcExt, values: any[]): Promise<QueueTxResult> {
const { api } = this.props;
try {
assert(
isFunction((api.rpc as Record<string, unknown>)[section] && (api.rpc as Record<string, Record<string, unknown>>)[section][method]),
`api.rpc.${section}.${method} does not exist`
);
const result = await (api.rpc as Record<string, Record<string, (...params: unknown[]) => Promise<unknown>>>)[section][method](...values);
console.log('submitRpc: result ::', format(result));
return {
result,
status: 'sent'
};
} catch (error) {
console.error(error);
return {
error: error as Error,
status: 'error'
};
}
}
private async makeExtrinsicCall (extrinsic: SubmittableExtrinsic, { id, txFailedCb, txStartCb, txSuccessCb, txUpdateCb }: QueueTx, extrinsicCall: (...params: any[]) => Promise<() => void>, pair?: KeyringPair): Promise<void> {
const { api, queueSetTxStatus } = this.props;
const { showTip, tip } = this.state;
console.log('makeExtrinsicCall: extrinsic ::', extrinsic.toHex());
const params = [];
if (pair) {
const { address, meta: { isExternal, isHardware, isInjected, source } } = pair;
queueSetTxStatus(id, 'signing');
// set the signer
if (isHardware) {
api.setSigner(ledgerSigner);
params.push(address);
} else if (isExternal) {
queueSetTxStatus(id, 'qr');
api.setSigner({ signPayload: this.signQrPayload });
params.push(address);
} else if (isInjected) {
const injected = await web3FromSource(source as string);
assert(injected, `Unable to find a signer for ${address}`);
api.setSigner(injected.signer);
params.push(address);
} else {
params.push(pair);
}
}
if (showTip && tip) {
params.push({ tip } as Partial<SignerOptions>);
}
if (isFunction(txStartCb)) {
txStartCb();
}
try {
const unsubscribe = await extrinsicCall.apply(extrinsic, [
...params,
// eslint-disable-next-line @typescript-eslint/require-await
async (result: SubmittableResult): Promise<void> => {
if (!result || !result.status) {
return;
}
const status = result.status.type.toLowerCase() as QueueTxStatus;
console.log('makeExtrinsicCall: updated status ::', JSON.stringify(result));
queueSetTxStatus(id, status, result);
if (isFunction(txUpdateCb)) {
txUpdateCb(result);
}
if (result.status.isFinalized || result.status.isInBlock) {
unsubscribe();
result.events
.filter(({ event: { section } }): boolean => section === 'system')
.forEach(({ event: { method } }): void => {
if (isFunction(txFailedCb) && method === 'ExtrinsicFailed') {
txFailedCb(result);
} else if (isFunction(txSuccessCb) && method === 'ExtrinsicSuccess') {
txSuccessCb(result);
}
});
} else if (result.isError && isFunction(txFailedCb)) {
txFailedCb(result);
}
}
]);
queueSetTxStatus(id, 'sending');
} catch (error) {
console.error('makeExtrinsicCall: error:', error);
queueSetTxStatus(id, 'error', {}, error);
if (isFunction(txFailedCb)) {
txFailedCb(null);
}
}
}
private async makeSignedTransaction (extrinsic: SubmittableExtrinsic, { id, txFailedCb, txStartCb }: QueueTx, pair: KeyringPair): Promise<void> {
const { queueSetTxStatus } = this.props;
const { blocks, nonce, showTip, tip } = this.state;
console.log('makeSignedTransaction: extrinsic ::', extrinsic.toHex());
const { address, meta: { isExternal, isHardware, isInjected, source } } = pair;
let signer: ApiSigner | undefined;
if (isFunction(txStartCb)) {
txStartCb();
}
// set the signer
if (isHardware) {
signer = ledgerSigner;
} else if (isExternal) {
queueSetTxStatus(id, 'qr');
signer = { signPayload: this.signQrPayload };
} else if (isInjected) {
const injected = await web3FromSource(source as string);
signer = injected?.signer;
}
assert(signer || pair, `Unable to find a signer for ${address}`);
try {
await extrinsic.signAsync((signer ? address : pair), {
era: blocks.toNumber(),
nonce: nonce || 0,
signer,
tip: (showTip && tip) ? tip : undefined
});
const signedTx = extrinsic.toJSON()?.toString();
console.log('makeSignedTransaction: result ::', signedTx);
this.setState({ signedTx });
} catch (e) {
queueSetTxStatus(id, 'error', undefined, e);
if (isFunction(txFailedCb)) {
txFailedCb(e);
}
}
}
}
export { Signer };
export default withMulti(
Signer,
translate,
withApi,
withObservable(keyring.accounts.subject, { propName: 'allAccounts' })
);

View File

@ -12,13 +12,13 @@ import keyring from '@polkadot/ui-keyring';
import { useTranslation } from './translate';
interface Props {
address: string;
className?: string;
error?: string;
onChange: (password: string) => void;
onEnter?: () => void;
password: string;
tabIndex?: number;
value?: string | null;
}
function getPair (address?: string | null): KeyringPair | null {
@ -29,38 +29,41 @@ function getPair (address?: string | null): KeyringPair | null {
}
}
function Unlock ({ className = '', error, onChange, onEnter, password, tabIndex, value }: Props): React.ReactElement<Props> | null {
function Unlock ({ address, className, error, onChange, onEnter, tabIndex }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const [pair, setPair] = useState<KeyringPair | null>(null);
const [password, setPassword] = useState('');
useEffect((): void => {
setPair(getPair(value));
}, [value]);
setPair(getPair(address));
}, [address]);
if (!pair || !(pair.isLocked) || pair.meta.isInjected) {
useEffect((): void => {
onChange(password);
}, [onChange, password]);
if (!pair || !pair.isLocked || pair.meta.isInjected) {
return null;
}
return (
<div className={`ui--signer-Signer-Unlock ${className}`}>
<Modal.Columns>
<Modal.Column>
<Password
autoFocus
isError={!!error}
label={t<string>('unlock account with password')}
labelExtra={error && <div className='errorLabel'>{t<string>('wrong password supplied')}</div>}
onChange={onChange}
onEnter={onEnter}
tabIndex={tabIndex}
value={password}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('Unlock the sending account to allow signing of this transaction.')}</p>
</Modal.Column>
</Modal.Columns>
</div>
<Modal.Columns className={className}>
<Modal.Column>
<Password
autoFocus
isError={!!error}
label={t('unlock account with password')}
labelExtra={error && <div className='errorLabel'>{t('wrong password supplied')}</div>}
onChange={setPassword}
onEnter={onEnter}
tabIndex={tabIndex}
value={password}
/>
</Modal.Column>
<Modal.Column>
<p>{t('Unlock the sending account to allow signing of this transaction.')}</p>
</Modal.Column>
</Modal.Columns>
);
}

View File

@ -9,7 +9,7 @@ import BN from 'bn.js';
import React, { useState, useEffect } from 'react';
import { Trans } from 'react-i18next';
import { Expander } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { useApi, useIsMountedRef } from '@polkadot/react-hooks';
import { formatBalance, isFunction } from '@polkadot/util';
interface Props {
@ -21,18 +21,22 @@ interface Props {
tip?: BN;
}
function Checks ({ accountId, className = '', extrinsic }: Props): React.ReactElement<Props> | null {
function PaymentInfo ({ accountId, className = '', extrinsic }: Props): React.ReactElement<Props> | null {
const { api } = useApi();
const [dispatchInfo, setDispatchInfo] = useState<RuntimeDispatchInfo | null>(null);
const mountedRef = useIsMountedRef();
useEffect((): void => {
accountId && extrinsic && isFunction(api.rpc.payment?.queryInfo) &&
// eslint-disable-next-line @typescript-eslint/unbound-method
accountId && extrinsic && isFunction(extrinsic.paymentInfo) && isFunction(api.rpc.payment?.queryInfo) &&
Promise.resolve(
extrinsic
.paymentInfo(accountId)
.then(setDispatchInfo)
).catch(console.error);
}, [api, accountId, extrinsic]);
.then((info): void => {
mountedRef.current && setDispatchInfo(info);
})
).catch((error: Error) => console.error(error.message));
}, [api, accountId, extrinsic, mountedRef]);
if (!dispatchInfo) {
return null;
@ -51,4 +55,4 @@ function Checks ({ accountId, className = '', extrinsic }: Props): React.ReactEl
);
}
export default React.memo(Checks);
export default React.memo(PaymentInfo);

View File

@ -6,6 +6,7 @@ import { BareProps } from '@polkadot/react-components/types';
import React from 'react';
import styled from 'styled-components';
import { Columar } from '@polkadot/react-components';
import { QrDisplayPayload, QrScanSignature } from '@polkadot/react-qr';
interface Props extends BareProps {
@ -21,13 +22,12 @@ interface Props extends BareProps {
const CMD_HASH = 1;
const CMD_MORTAL = 2;
function Qr ({ address, className = '', genesisHash, isHashed, isScanning, onSignature, payload }: Props): React.ReactElement<Props> {
function Qr ({ address, className, genesisHash, isHashed, onSignature, payload }: Props): React.ReactElement<Props> {
return (
<div className={className}>
{
isScanning
? <QrScanSignature onScan={onSignature} />
: <QrDisplayPayload
<Columar className={className}>
<Columar.Column>
<div className='qrDisplay'>
<QrDisplayPayload
address={address}
cmd={
isHashed
@ -37,12 +37,20 @@ function Qr ({ address, className = '', genesisHash, isHashed, isScanning, onSig
genesisHash={genesisHash}
payload={payload}
/>
}
</div>
</div>
</Columar.Column>
<Columar.Column>
<div className='qrDisplay'>
<QrScanSignature onScan={onSignature} />
</div>
</Columar.Column>
</Columar>
);
}
export default React.memo(styled(Qr)`
margin: 0 auto;
max-width: 30rem;
.qrDisplay {
margin: 0 auto;
max-width: 30rem;
}
`);

View File

@ -0,0 +1,86 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { SignerOptions } from '@polkadot/api/submittable/types';
import BN from 'bn.js';
import React, { useCallback, useEffect, useState } from 'react';
import { InputNumber, Modal, Output } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { BN_ZERO } from '@polkadot/util';
import { useTranslation } from './translate';
interface Props {
address: string | null;
className?: string;
onChange: (signedOptions: Partial<SignerOptions>) => void;
signedTx: string | null;
}
function SignFields ({ address, onChange, signedTx }: Props): React.ReactElement<Props> {
const { api } = useApi();
const [blocks, setBlocks] = useState(new BN(64));
const [nonce, setNonce] = useState(BN_ZERO);
const { t } = useTranslation();
useEffect((): void => {
address && api.derive.balances
.account(address)
.then(({ accountNonce }) => setNonce(accountNonce))
.catch(console.error);
}, [address, api]);
useEffect((): void => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
onChange({ era: blocks.toNumber(), nonce });
}, [blocks, nonce, onChange]);
const _setBlocks = useCallback(
(blocks = BN_ZERO) => setBlocks(blocks),
[]
);
const _setNonce = useCallback(
(nonce = BN_ZERO) => setNonce(nonce),
[]
);
return (
<>
<Modal.Columns>
<Modal.Column>
<InputNumber
isDisabled={!!signedTx}
isZeroable
label={t('Nonce')}
labelExtra={t('Current account nonce: {{accountNonce}}', { replace: { accountNonce: nonce } })}
onChange={_setNonce}
value={nonce}
/>
<InputNumber
isDisabled={!!signedTx}
isZeroable
label={t('Lifetime (# of blocks)')}
labelExtra={t('Set to 0 to make transaction immortal')}
onChange={_setBlocks}
value={blocks}
/>
</Modal.Column>
<Modal.Column>
<p>{t('Override any applicable values for the specific signed output. These will be used to construct and display the signed transaction.')}</p>
</Modal.Column>
</Modal.Columns>
{!!signedTx && (
<Output
label={t<string>('Signed transaction')}
value={signedTx}
withCopy
/>
)}
</>
);
}
export default React.memo(SignFields);

View File

@ -0,0 +1,55 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import { InputBalance, Modal, Toggle } from '@polkadot/react-components';
import { BN_ZERO } from '@polkadot/util';
import { useTranslation } from './translate';
interface Props {
className?: string;
onChange: (tip: BN) => void;
}
function Tip ({ className, onChange }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const [tip, setTip] = useState(BN_ZERO);
const [showTip, setShowTip] = useState(false);
useEffect((): void => {
onChange(showTip ? tip : BN_ZERO);
}, [onChange, showTip, tip]);
return (
<Modal.Columns className={className}>
<Modal.Column>
<Toggle
className='tipToggle'
label={
showTip
? t<string>('Include an optional tip for faster processing')
: t<string>('Do not include a tip for the block author')
}
onChange={setShowTip}
value={showTip}
/>
{showTip && (
<InputBalance
help={t<string>('Add a tip to this extrinsic, paying the block author for greater priority')}
isZeroable
label={t<string>('Tip (optional)')}
onChange={setTip}
/>
)}
</Modal.Column>
<Modal.Column>
<p>{t<string>('Adding an optional tip to the transaction could allow for higher priority, especially when the chain is busy.')}</p>
</Modal.Column>
</Modal.Columns>
);
}
export default React.memo(Tip);

View File

@ -8,22 +8,20 @@ import BN from 'bn.js';
import React from 'react';
import styled from 'styled-components';
import { registry } from '@polkadot/react-api';
import { Call, InputAddress, Expander, Modal } from '@polkadot/react-components';
import { Call, Expander, Modal } from '@polkadot/react-components';
import Checks from './Checks';
import { useTranslation } from './translate';
import PaymentInfo from './PaymentInfo';
interface Props {
children?: React.ReactNode;
className?: string;
hideDetails?: boolean;
currentItem: QueueTx;
isSendable: boolean;
onError: () => void;
tip?: BN;
value: QueueTx;
}
function Transaction ({ children, className = '', hideDetails, isSendable, onError, tip, value: { accountId, extrinsic, isUnsigned } }: Props): React.ReactElement<Props> | null {
function Transaction ({ className, currentItem: { accountId, extrinsic, isUnsigned, payload }, isSendable, onError, tip }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
if (!extrinsic) {
@ -31,75 +29,44 @@ function Transaction ({ children, className = '', hideDetails, isSendable, onErr
}
const { meta, method, section } = registry.findMetaCall(extrinsic.callIndex);
const args = meta?.args.map(({ name }) => name).join(', ') || '';
return (
<Modal.Content className={`ui--signer-Signer-Content ${className}`}>
{!hideDetails && (
<>
{!isUnsigned && accountId && (
<Modal.Columns>
<Modal.Column>
<InputAddress
className='full'
defaultValue={accountId}
isDisabled
isInput
label={t<string>('sending from my account')}
withLabel
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The sending account that will be used to send this transaction. Any applicable fees will be paid by this account.')}</p>
</Modal.Column>
</Modal.Columns>
)}
<Modal.Columns>
<Modal.Column>
<Expander
className='tx-details'
summary={
<>
{t<string>('Sending transaction')} <span className='highlight'>{section}.{method}({
meta?.args.map(({ name }) => name).join(', ') || ''
})</span>
</>
}
summaryMeta={meta}
>
<Call
onError={onError}
value={extrinsic}
withBorder={false}
/>
</Expander>
{!isUnsigned && (
<Checks
accountId={accountId}
className='tx-details'
extrinsic={extrinsic}
isSendable={isSendable}
tip={tip}
/>
)}
</Modal.Column>
<Modal.Column>
<p>{t<string>('The details of the transaction including the type, the description (as available from the chain metadata) as well as any parameters and fee estimations (as available) for the specific type of call.')}</p>
</Modal.Column>
</Modal.Columns>
</>
)}
{children}
</Modal.Content>
<Modal.Columns className={className}>
<Modal.Column>
<Expander
className='tx-details'
summary={<>{t<string>('Sending transaction')} <span className='highlight'>{section}.{method}({args})</span></>}
summaryMeta={meta}
>
<Call
onError={onError}
value={extrinsic}
withBorder={false}
/>
</Expander>
{!isUnsigned && !payload && (
<PaymentInfo
accountId={accountId}
className='tx-details'
extrinsic={extrinsic}
isSendable={isSendable}
tip={tip}
/>
)}
</Modal.Column>
<Modal.Column>
<p>{t<string>('The details of the transaction including the type, the description (as available from the chain metadata) as well as any parameters and fee estimations (as available) for the specific type of call.')}</p>
</Modal.Column>
</Modal.Columns>
);
}
export default React.memo(styled(Transaction)`
.tx-details {
margin-left: 2rem;
.ui--Expander-summary {
font-size: 1.1rem;
margin: 0.5rem 0;
margin: 0 0 0.5rem;
}
.highlight {

View File

@ -0,0 +1,366 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { SignerResult, SubmittableExtrinsic } from '@polkadot/api/types';
import { SignerOptions } from '@polkadot/api/submittable/types';
import { KeyringPair } from '@polkadot/keyring/types';
import { QueueTx, QueueTxMessageSetStatus } from '@polkadot/react-components/Status/types';
import { Multisig, Timepoint } from '@polkadot/types/interfaces';
import { SignerPayloadJSON } from '@polkadot/types/types';
import { AddressProxy } from './types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { ApiPromise } from '@polkadot/api';
import { web3FromSource } from '@polkadot/extension-dapp';
import { registry } from '@polkadot/react-api';
import { Button, ErrorBoundary, Modal, StatusContext, Toggle } from '@polkadot/react-components';
import { useApi, useToggle } from '@polkadot/react-hooks';
import { Option } from '@polkadot/types';
import keyring from '@polkadot/ui-keyring';
import { BN_ZERO, assert } from '@polkadot/util';
import { blake2AsU8a } from '@polkadot/util-crypto';
import ledgerSigner from './LedgerSigner';
import { useTranslation } from './translate';
import Address from './Address';
import Qr from './Qr';
import SignFields from './SignFields';
import Tip from './Tip';
import Transaction from './Transaction';
import { extractExternal, handleTxResults } from './util';
interface Props {
className?: string;
currentItem: QueueTx;
requestAddress: string;
}
interface QrState {
isQrHashed: boolean;
isQrVisible: boolean;
qrAddress: string;
qrPayload: Uint8Array;
qrResolve?: (result: SignerResult) => void;
qrReject?: (error: Error) => void;
}
const NOOP = () => undefined;
let qrId = 0;
function unlockAccount ({ signAddress, signPassword }: AddressProxy): string | null {
let publicKey;
try {
publicKey = keyring.decodeAddress(signAddress as string);
} catch (error) {
console.error(error);
return 'unable to decode address';
}
const pair = keyring.getPair(publicKey);
try {
pair.decodePkcs8(signPassword);
} catch (error) {
console.error(error);
return (error as Error).message;
}
return null;
}
async function signAndSend (queueSetTxStatus: QueueTxMessageSetStatus, currentItem: QueueTx, tx: SubmittableExtrinsic<'promise'>, pairOrAddress: KeyringPair | string, options: Partial<SignerOptions>): Promise<void> {
currentItem.txStartCb && currentItem.txStartCb();
try {
const unsubscribe = await tx.signAndSend(pairOrAddress, options, handleTxResults('signAndSend', queueSetTxStatus, currentItem, (): void => {
unsubscribe();
}));
} catch (error) {
console.error('signAndSend: error:', error);
queueSetTxStatus(currentItem.id, 'error', {}, error);
currentItem.txFailedCb && currentItem.txFailedCb(null);
}
}
async function signAsync (queueSetTxStatus: QueueTxMessageSetStatus, { id, txFailedCb = NOOP, txStartCb = NOOP }: QueueTx, tx: SubmittableExtrinsic<'promise'>, pairOrAddress: KeyringPair | string, options: Partial<SignerOptions>): Promise<string | null> {
txStartCb();
try {
return (await tx.signAsync(pairOrAddress, options)).toJSON();
} catch (error) {
queueSetTxStatus(id, 'error', undefined, error);
txFailedCb(error);
}
return null;
}
function signQrPayload (setQrState: (state: QrState) => void): (payload: SignerPayloadJSON) => Promise<SignerResult> {
return (payload: SignerPayloadJSON): Promise<SignerResult> =>
new Promise((resolve, reject): void => {
// limit size of the transaction
const isQrHashed = (payload.method.length > 5000);
const wrapper = registry.createType('ExtrinsicPayload', payload, { version: payload.version });
const qrPayload = isQrHashed
? blake2AsU8a(wrapper.toU8a(true))
: wrapper.toU8a();
setQrState({
isQrHashed,
isQrVisible: true,
qrAddress: payload.address,
qrPayload,
qrReject: reject,
qrResolve: resolve
});
});
}
async function wrapTx (api: ApiPromise, currentItem: QueueTx, { isMultiCall, multiRoot, proxyRoot, signAddress }: AddressProxy): Promise<SubmittableExtrinsic<'promise'>> {
let tx = currentItem.extrinsic as SubmittableExtrinsic<'promise'>;
if (proxyRoot) {
tx = api.tx.proxy.proxy(proxyRoot, null, tx);
}
if (multiRoot) {
const multiModule = api.tx.multisig ? 'multisig' : 'utility';
const info = await api.query[multiModule].multisigs<Option<Multisig>>(multiRoot, tx.method.hash);
const { threshold, who } = extractExternal(multiRoot);
const others = who.filter((w) => w !== signAddress);
let timepoint: Timepoint | null = null;
if (info.isSome) {
timepoint = info.unwrap().when;
}
tx = isMultiCall
? api.tx[multiModule].asMulti.meta.args.length === 5
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
? api.tx[multiModule].asMulti(threshold, others, timepoint, tx.method, false)
: api.tx[multiModule].asMulti(threshold, others, timepoint, tx.method)
: api.tx[multiModule].approveAsMulti(threshold, others, timepoint, tx.method.hash);
}
return tx;
}
async function extractParams (address: string, options: Partial<SignerOptions>, setQrState: (state: QrState) => void): Promise<['qr' | 'signing', KeyringPair | string, Partial<SignerOptions>]> {
const pair = keyring.getPair(address);
const { meta: { isExternal, isHardware, isInjected, source } } = pair;
if (isHardware) {
return ['signing', address, { ...options, signer: ledgerSigner }];
} else if (isExternal) {
return ['qr', address, { ...options, signer: { signPayload: signQrPayload(setQrState) } }];
} else if (isInjected) {
const injected = await web3FromSource(source as string);
assert(injected, `Unable to find a signer for ${address}`);
return ['signing', address, { ...options, signer: injected.signer }];
}
return ['signing', pair, options];
}
function TxSigned ({ className, currentItem, requestAddress }: Props): React.ReactElement<Props> | null {
const { api } = useApi();
const { t } = useTranslation();
const { queueSetTxStatus } = useContext(StatusContext);
const [flags, setFlags] = useState(extractExternal(requestAddress));
const [{ isQrHashed, isQrVisible, qrAddress, qrPayload, qrResolve }, setQrState] = useState<QrState>({ isQrHashed: false, isQrVisible: false, qrAddress: '', qrPayload: new Uint8Array() });
const [isRenderError, toggleRenderError] = useToggle();
const [isSubmit, setIsSubmit] = useState(true);
const [passwordError, setPasswordError] = useState<string | null>(null);
const [senderInfo, setSenderInfo] = useState<AddressProxy>({ isMultiCall: false, multiRoot: null, proxyRoot: null, signAddress: requestAddress, signPassword: '' });
const [signedOptions, setSignedOptions] = useState<Partial<SignerOptions>>({});
const [signedTx, setSignedTx] = useState<string | null>(null);
const [tip, setTip] = useState(BN_ZERO);
useEffect((): void => {
setFlags(extractExternal(senderInfo.signAddress));
setPasswordError(null);
}, [senderInfo]);
const _addQrSignature = useCallback(
({ signature }: { signature: string }) => qrResolve && qrResolve({
id: ++qrId,
signature
}),
[qrResolve]
);
const _onCancel = useCallback(
(): void => {
const { id, signerCb = NOOP, txFailedCb = NOOP } = currentItem;
queueSetTxStatus(id, 'cancelled');
signerCb(id, null);
txFailedCb(null);
},
[currentItem, queueSetTxStatus]
);
const _unlock = useCallback(
(): boolean => {
const passwordError = senderInfo.signAddress && flags.isUnlockable
? unlockAccount(senderInfo)
: null;
setPasswordError(passwordError);
return !passwordError;
},
[flags, senderInfo]
);
const _onSendPayload = useCallback(
(): void => {
if (_unlock() && senderInfo.signAddress && currentItem.payload) {
const { id, payload, signerCb = NOOP } = currentItem;
const pair = keyring.getPair(senderInfo.signAddress);
const result = registry.createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair);
signerCb(id, { id, ...result });
queueSetTxStatus(id, 'completed');
}
},
[_unlock, currentItem, queueSetTxStatus, senderInfo]
);
const _onSend = useCallback(
async (): Promise<void> => {
if (_unlock() && senderInfo.signAddress) {
const [tx, [status, pairOrAddress, options]] = await Promise.all([
wrapTx(api, currentItem, senderInfo),
extractParams(senderInfo.signAddress, { tip }, setQrState)
]);
queueSetTxStatus(currentItem.id, status);
await signAndSend(queueSetTxStatus, currentItem, tx, pairOrAddress, options);
}
},
[_unlock, api, currentItem, queueSetTxStatus, senderInfo, tip]
);
const _onSign = useCallback(
async (): Promise<void> => {
if (_unlock() && senderInfo.signAddress) {
const [tx, [, pairOrAddress, options]] = await Promise.all([
wrapTx(api, currentItem, senderInfo),
extractParams(senderInfo.signAddress, { ...signedOptions, tip }, setQrState)
]);
setSignedTx(await signAsync(queueSetTxStatus, currentItem, tx, pairOrAddress, options));
}
},
[_unlock, api, currentItem, queueSetTxStatus, senderInfo, signedOptions, tip]
);
return (
<>
<Modal.Content className={className}>
<ErrorBoundary onError={toggleRenderError}>
{isQrVisible
? (
<Qr
address={qrAddress}
genesisHash={api.genesisHash}
isHashed={isQrHashed}
onSignature={_addQrSignature}
payload={qrPayload}
/>
)
: (
<>
<Transaction
currentItem={currentItem}
onError={toggleRenderError}
/>
<Address
currentItem={currentItem}
onChange={setSenderInfo}
passwordError={passwordError}
requestAddress={requestAddress}
/>
{!currentItem.payload && (
<Tip onChange={setTip} />
)}
{!isSubmit && (
<SignFields
address={senderInfo.signAddress}
onChange={setSignedOptions}
signedTx={signedTx}
/>
)}
</>
)
}
</ErrorBoundary>
</Modal.Content>
<Modal.Actions onCancel={_onCancel}>
{!isQrVisible && (
<>
<Button
icon={
flags.isQr
? 'qrcode'
: 'sign-in'
}
isDisabled={!senderInfo.signAddress || isRenderError}
isPrimary
label={
flags.isQr
? t<string>('Sign via Qr')
: isSubmit
? t<string>('Sign and Submit')
: t<string>('Sign (no submission)')
}
onClick={
isSubmit
? currentItem.payload
? _onSendPayload
: _onSend
: _onSign
}
tabIndex={2}
/>
<Toggle
className='signToggle'
isDisabled={isQrVisible || !!currentItem.payload}
label={
isSubmit
? t<string>('Sign and Submit')
: t<string>('Sign (no submission)')
}
onChange={setIsSubmit}
value={isSubmit}
/>
</>
)}
</Modal.Actions>
</>
);
}
export default React.memo(styled(TxSigned)`
.tipToggle {
width: 100%;
text-align: right;
}
.ui--Checks {
margin-top: 0.75rem;
}
`);

View File

@ -0,0 +1,87 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { SubmittableExtrinsic } from '@polkadot/api/types';
import { QueueTx, QueueTxMessageSetStatus } from '@polkadot/react-components/Status/types';
import React, { useCallback, useContext } from 'react';
import { Button, ErrorBoundary, Modal, StatusContext } from '@polkadot/react-components';
import { useToggle } from '@polkadot/react-hooks';
import { useTranslation } from './translate';
import Transaction from './Transaction';
import { handleTxResults } from './util';
interface Props {
className?: string;
currentItem: QueueTx;
}
const NOOP = () => undefined;
async function send (queueSetTxStatus: QueueTxMessageSetStatus, currentItem: QueueTx, tx: SubmittableExtrinsic<'promise'>): Promise<void> {
currentItem.txStartCb && currentItem.txStartCb();
try {
const unsubscribe = await tx.send(handleTxResults('send', queueSetTxStatus, currentItem, (): void => {
unsubscribe();
}));
} catch (error) {
console.error('send: error:', error);
queueSetTxStatus(currentItem.id, 'error', {}, error);
currentItem.txFailedCb && currentItem.txFailedCb(null);
}
}
function TxUnsigned ({ className, currentItem }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { queueSetTxStatus } = useContext(StatusContext);
const [isRenderError, toggleRenderError] = useToggle();
const _onCancel = useCallback(
(): void => {
const { id, signerCb = NOOP, txFailedCb = NOOP } = currentItem;
queueSetTxStatus(id, 'cancelled');
signerCb(id, null);
txFailedCb(null);
},
[currentItem, queueSetTxStatus]
);
const _onSend = useCallback(
async (): Promise<void> => {
if (currentItem.extrinsic) {
await send(queueSetTxStatus, currentItem, currentItem.extrinsic);
}
},
[currentItem, queueSetTxStatus]
);
return (
<>
<Modal.Content className={className}>
<ErrorBoundary onError={toggleRenderError}>
<Transaction
currentItem={currentItem}
onError={toggleRenderError}
/>
</ErrorBoundary>
</Modal.Content>
<Modal.Actions onCancel={_onCancel}>
<Button
icon='sign-in'
isDisabled={isRenderError}
isPrimary
label={t('Submit (no signature)')}
onClick={_onSend}
tabIndex={2}
/>
</Modal.Actions>
</>
);
}
export default React.memo(TxUnsigned);

View File

@ -2,70 +2,118 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { QueueTx, QueueTxMessageSetStatus, QueueTxResult } from '@polkadot/react-components/Status/types';
import { BareProps } from '@polkadot/react-components/types';
import { DefinitionRpcExt } from '@polkadot/types/types';
import React, { useContext } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { StatusContext } from '@polkadot/react-components';
import { ApiPromise } from '@polkadot/api';
import { Modal, StatusContext } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { assert, isFunction } from '@polkadot/util';
import { format } from '@polkadot/util/logger';
import Modal from './Modal';
import { useTranslation } from './translate';
import TxSigned from './TxSigned';
import TxUnsigned from './TxUnsigned';
interface Props extends BareProps {
children: React.ReactNode;
}
interface ItemState {
currentItem: QueueTx | null;
requestAddress: string | null;
}
async function submitRpc (api: ApiPromise, { method, section }: DefinitionRpcExt, values: any[]): Promise<QueueTxResult> {
try {
const rpc = api.rpc as Record<string, Record<string, (...params: unknown[]) => Promise<unknown>>>;
assert(isFunction(rpc[section] && rpc[section][method]), `api.rpc.${section}.${method} does not exist`);
const result = await rpc[section][method](...values);
console.log('submitRpc: result ::', format(result));
return {
result,
status: 'sent'
};
} catch (error) {
console.error(error);
return {
error: error as Error,
status: 'error'
};
}
}
async function sendRpc (api: ApiPromise, queueSetTxStatus: QueueTxMessageSetStatus, { id, rpc, values = [] }: QueueTx): Promise<void> {
if (rpc) {
queueSetTxStatus(id, 'sending');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { error, result, status } = await submitRpc(api, rpc, values);
queueSetTxStatus(id, status, result, error);
}
}
function extractCurrent (api: ApiPromise, queueSetTxStatus: QueueTxMessageSetStatus, txqueue: QueueTx[]): ItemState {
const nextItem = txqueue.find(({ status }) => ['queued', 'qr'].includes(status)) || null;
let currentItem = null;
// when the next up is an RPC, send it immediately
if (nextItem && nextItem.status === 'queued' && !(nextItem.extrinsic || nextItem.payload)) {
sendRpc(api, queueSetTxStatus, nextItem).catch(console.error);
} else {
currentItem = nextItem;
}
return {
currentItem,
requestAddress: (currentItem && currentItem.accountId) || null
};
}
function Signer ({ children, className = '' }: Props): React.ReactElement<Props> {
const { api } = useApi();
const { t } = useTranslation();
const { queueSetTxStatus, txqueue } = useContext(StatusContext);
const [{ currentItem, requestAddress }, setItem] = useState<ItemState>({ currentItem: null, requestAddress: null });
useEffect((): void => {
setItem(extractCurrent(api, queueSetTxStatus, txqueue));
}, [api, queueSetTxStatus, txqueue]);
return (
<>
{children}
<Modal
className={className}
key='signer-modal'
queue={txqueue}
queueSetTxStatus={queueSetTxStatus}
/>
{currentItem && (
<Modal
className={className}
header={t('Authorize transaction')}
size='large'
>
{currentItem.isUnsigned
? <TxUnsigned currentItem={currentItem} />
: (
<TxSigned
currentItem={currentItem}
requestAddress={requestAddress}
/>
)
}
</Modal>
)}
</>
);
}
export default React.memo(styled(Signer)`
.ui--signer-Signer-Content {
.modal-Text {
margin-bottom: 1em;
padding: 1em;
}
.expanded h3 {
margin-bottom: 0.75rem;
}
.code {
background: #f5f4f3;
font-family: monospace;
margin: 0 0.25em;
overflow-wrap: break-word;
padding: 0.25em 0.5em;
word-break: break-all;
word-break: break-word;
word-wrap: break-word;
}
.hl {
font-weight: 700;
}
.tipToggle {
width: 100%;
text-align: right;
}
.ui--Checks {
margin-top: 0.75rem;
}
}
.signToggle {
position: absolute;
left: 1.5rem;

View File

@ -2,6 +2,24 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
export interface AddressFlags {
hardwareType?: string;
isHardware: boolean;
isMultisig: boolean;
isQr: boolean;
isUnlockable: boolean;
threshold: number;
who: string[];
}
export interface AddressProxy {
isMultiCall: boolean;
multiRoot: string | null;
proxyRoot: string | null;
signAddress: string | null;
signPassword: string;
}
export interface Signed {
data: Uint8Array;
message: Uint8Array;

View File

@ -0,0 +1,76 @@
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { QueueTx, QueueTxMessageSetStatus, QueueTxStatus } from '@polkadot/react-components/Status/types';
import { AddressFlags } from './types';
import { SubmittableResult } from '@polkadot/api';
import keyring from '@polkadot/ui-keyring';
const NOOP = () => undefined;
export function extractExternal (accountId: string | null): AddressFlags {
if (!accountId) {
return { isHardware: false, isMultisig: false, isQr: false, isUnlockable: false, threshold: 0, who: [] };
}
let publicKey;
try {
publicKey = keyring.decodeAddress(accountId);
} catch (error) {
console.error(error);
return { isHardware: false, isMultisig: false, isQr: false, isUnlockable: false, threshold: 0, who: [] };
}
const pair = keyring.getPair(publicKey);
return {
hardwareType: pair.meta.hardwareType as string,
isHardware: !!pair.meta.isHardware,
isMultisig: !!pair.meta.isMultisig,
isQr: !!pair.meta.isExternal && !pair.meta.isMultisig,
isUnlockable: !pair.meta.isExternal && !pair.meta.isHardware && !pair.meta.isInjected && pair.isLocked,
threshold: (pair.meta.threshold as number) || 0,
who: ((pair.meta.who as string[]) || []).map(recodeAddress)
};
}
export function recodeAddress (address: string | Uint8Array): string {
return keyring.encodeAddress(keyring.decodeAddress(address));
}
export function handleTxResults (handler: 'send' | 'signAndSend', queueSetTxStatus: QueueTxMessageSetStatus, { id, txFailedCb = NOOP, txSuccessCb = NOOP, txUpdateCb = NOOP }: QueueTx, unsubscribe: () => void): (result: SubmittableResult) => void {
return (result: SubmittableResult): void => {
if (!result || !result.status) {
return;
}
const status = result.status.type.toLowerCase() as QueueTxStatus;
console.log(`${handler}: status :: ${JSON.stringify(result)}`);
queueSetTxStatus(id, status, result);
txUpdateCb(result);
if (result.status.isFinalized || result.status.isInBlock) {
result.events
.filter(({ event: { section } }) => section === 'system')
.forEach(({ event: { method } }): void => {
if (method === 'ExtrinsicFailed') {
txFailedCb(result);
} else if (method === 'ExtrinsicSuccess') {
txSuccessCb(result);
}
});
} else if (result.isError) {
txFailedCb(result);
}
if (result.isCompleted) {
unsubscribe();
}
};
}