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:
parent
dcf2f80d2b
commit
601ae6da08
@ -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
|
||||
|
||||
@ -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' })
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
279
packages/react-signer/src/Address.tsx
Normal file
279
packages/react-signer/src/Address.tsx
Normal 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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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' })
|
||||
);
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
@ -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;
|
||||
}
|
||||
`);
|
||||
|
||||
86
packages/react-signer/src/SignFields.tsx
Normal file
86
packages/react-signer/src/SignFields.tsx
Normal 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);
|
||||
55
packages/react-signer/src/Tip.tsx
Normal file
55
packages/react-signer/src/Tip.tsx
Normal 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);
|
||||
@ -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 {
|
||||
|
||||
366
packages/react-signer/src/TxSigned.tsx
Normal file
366
packages/react-signer/src/TxSigned.tsx
Normal 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;
|
||||
}
|
||||
`);
|
||||
87
packages/react-signer/src/TxUnsigned.tsx
Normal file
87
packages/react-signer/src/TxUnsigned.tsx
Normal 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);
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
76
packages/react-signer/src/util.ts
Normal file
76
packages/react-signer/src/util.ts
Normal 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user