Basic address management app (#38)

* Update vanity regex (no 'I')

* Allow copy of generated seed

* Update connecting info

* ui--form -> ui--row

* Cleanup layouts with ui--row

* negative -> default (non-removal)

* Embedded rows don't have padding

* Allow ability to hide address

* Adjust button layouts

* Make account edit/create (slightly) prettier

* @flow fixes

* Some spacing adjustments

* Outline ofg the addressbook app

* Address create & edit operational

* @flow fixes

* Small UI cleanups, done as a first round
This commit is contained in:
Jaco Greeff 2018-05-11 09:20:44 +02:00 committed by GitHub
parent cd64a4de5f
commit 0eab4f6f22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1016 additions and 235 deletions

5
flow-typed/react-copy-to-clipboard.js vendored Normal file
View File

@ -0,0 +1,5 @@
// @flow
declare module 'react-copy-to-clipboard' {
declare module.exports: React$ComponentType<{ text: string }>;
}

View File

@ -6,36 +6,60 @@
import type { BareProps } from '@polkadot/ui-react-app/types';
import React from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import Button from 'semantic-ui-react/dist/es/elements/Button';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import Balance from '@polkadot/ui-react-rx/Balance';
import Nonce from '@polkadot/ui-react-rx/Nonce';
type Props = BareProps & {
address: string | null;
value: null | {
address: string,
publicKey: Uint8Array
};
}
export default function Address ({ address, className, style }: Props): React$Node {
export default function Address ({ className, style, value }: Props): React$Node {
if (!value) {
return null;
}
const { address, publicKey } = value;
const short = `${address.slice(0, 7)}…${address.slice(-7)}`;
return (
<div
className={['accounts--Address', 'ui disabled dropdown selection', className].join(' ')}
className={['accounts--Address', className].join(' ')}
style={style}
>
{
// flowlint-next-line sketchy-null-string:off
address
? (
<IdentityIcon
className='accounts--Address-icon'
size={32}
value={address}
/>
)
: null
}
<div className='accounts--Address-name'>
{
// flowlint-next-line sketchy-null-string:off
address || 'unknown'
}
<IdentityIcon
className='accounts--Address-icon'
size={96}
value={address}
/>
<div className='accounts--Address-data'>
<div className='accounts--Address-address'>
{short}
</div>
<CopyToClipboard text={address}>
<Button
icon='copy'
primary
size='tiny'
/>
</CopyToClipboard>
</div>
<Balance
className='accounts--Address-balance'
label='balance '
value={publicKey}
/>
<Nonce
className='accounts--Address-nonce'
value={publicKey}
>
{' transactions'}
</Nonce>
</div>
);
}

View File

@ -27,6 +27,7 @@ type Props = I18nProps & {
type State = {
address: string | null,
publicKey: Uint8Array | null,
fieldName: string,
isNameValid: boolean,
isSeedValid: boolean,
@ -44,10 +45,14 @@ function formatSeed (seed: string): Uint8Array {
: u8aFromString(seed.padEnd(32, ' '));
}
function addressFromSeed (seed: string): string {
function infoFromSeed (seed: string): { address: string, publicKey: Uint8Array } {
const { publicKey } = keypairFromSeed(formatSeed(seed));
const address = addressEncode(publicKey);
return addressEncode(publicKey);
return {
address,
publicKey
};
}
class Creator extends React.PureComponent<Props, State> {
@ -61,72 +66,80 @@ class Creator extends React.PureComponent<Props, State> {
render (): React$Node {
const { className, style, t } = this.props;
const { address, fieldName, isNameValid, isPassValid, isPassVisible, isSeedValid, isValid, name, password, seed } = this.state;
const { address, fieldName, isNameValid, isPassValid, isPassVisible, isSeedValid, isValid, name, password, publicKey, seed } = this.state;
return (
<div
className={['accounts--Creator', className].join(' ')}
style={style}
>
<div className='ui--form'>
<div className='ui--grid'>
<div className='medium'>
<Label>{t('creator.seed', {
defaultValue: 'create from the following seed (hex or string)'
})}</Label>
<Input
error={!isSeedValid}
name={`${fieldName}_seed`}
onChange={this.onChangeSeed}
value={seed}
/>
</div>
<div className='medium'>
<Label>{t('creator.address', {
defaultValue: 'evaluating to address'
})}</Label>
<Address address={address} />
<div className='ui--row'>
<div className='full'>
<Label>{t('creator.seed', {
defaultValue: 'create from the following seed (hex or string)'
})}</Label>
<Input
error={!isSeedValid}
name={`${fieldName}_seed`}
onChange={this.onChangeSeed}
value={seed}
/>
</div>
</div>
<div className='ui--row'>
<div className='full'>
<Label>{t('creator.name', {
defaultValue: 'name the account'
})}</Label>
<Input
error={!isNameValid}
name={`${fieldName}_name`}
onChange={this.onChangeName}
value={name}
/>
</div>
</div>
<div className='ui--row'>
<div className='full'>
<Label>{t('creator.pass1', {
defaultValue: 'encrypt it using the password'
})}</Label>
<Input
action
error={!isPassValid}
name={`${fieldName}_pass`}
onChange={this.onChangePass}
type={isPassVisible ? 'text' : 'password'}
value={password}
>
<input />
<Button
icon='eye'
primary
onClick={this.togglePassword}
/>
</Input>
</div>
</div>
</div>
<Address
className='medium'
value={
// flowlint-next-line sketchy-null-string:off
!address || !publicKey
? null
: {
address,
publicKey
}
}
/>
</div>
<div className='ui--form'>
<div className='medium'>
<Label>{t('creator.name', {
defaultValue: 'name the account'
})}</Label>
<Input
error={!isNameValid}
name={`${fieldName}_name`}
onChange={this.onChangeName}
value={name}
/>
</div>
</div>
<div className='ui--form'>
<div className='medium'>
<Label>{t('creator.pass1', {
defaultValue: 'encrypt it using the password'
})}</Label>
<Input
action
error={!isPassValid}
name={`${fieldName}_pass`}
onChange={this.onChangePass}
type={isPassVisible ? 'text' : 'password'}
value={password}
>
<input />
<Button
icon='eye'
primary
onClick={this.togglePassword}
/>
</Input>
</div>
</div>
<div className='ui--form-buttons'>
<div className='ui--row-buttons'>
<Button
negative
onClick={this.onDiscard}
primary
>
{t('creator.discard', {
defaultValue: 'Reset'
@ -148,7 +161,7 @@ class Creator extends React.PureComponent<Props, State> {
emptyState (): State {
const seed = u8aToHex(randomBytes());
const address = addressFromSeed(seed);
const { address, publicKey } = infoFromSeed(seed);
return {
address,
@ -160,6 +173,7 @@ class Creator extends React.PureComponent<Props, State> {
isValid: false,
name: 'new keypair',
password: '',
publicKey,
seed
};
}
@ -169,6 +183,7 @@ class Creator extends React.PureComponent<Props, State> {
(prevState: State, props: Props): $Shape<State> => {
const { name = prevState.name, password = prevState.password, seed = prevState.seed } = newState;
let address = prevState.address;
let publicKey = prevState.publicKey;
const isNameValid = !!name;
const isSeedValid = isHex(seed)
? seed.length === 66
@ -176,7 +191,10 @@ class Creator extends React.PureComponent<Props, State> {
const isPassValid = password.length > 0 && password.length <= 32;
if (isSeedValid && seed !== prevState.seed) {
address = addressFromSeed(seed);
const info = infoFromSeed(seed);
address = info.address;
publicKey = info.publicKey;
}
return {
@ -187,6 +205,7 @@ class Creator extends React.PureComponent<Props, State> {
isValid: isNameValid && isPassValid && isSeedValid,
name,
password,
publicKey,
seed
};
}

View File

@ -13,6 +13,7 @@ import Label from 'semantic-ui-react/dist/es/elements/Label';
import keyring from '@polkadot/ui-keyring/src';
import InputAddress from '@polkadot/ui-react-app/src/InputAddress';
import Address from './Address';
import translate from './translate';
type Props = I18nProps & {
@ -41,41 +42,52 @@ class Editor extends React.PureComponent<Props, State> {
render (): React$Node {
const { className, style, t } = this.props;
const { defaultPublicKey, editedName, isEdited } = this.state;
const { defaultPublicKey, currentPair, editedName, isEdited } = this.state;
return (
<div
className={['accounts--Editor', className].join(' ')}
style={style}
>
<div className='ui--form'>
<InputAddress
defaultValue={defaultPublicKey}
isInput={false}
label={t('editor.select', {
defaultValue: 'using my account'
})}
onChange={this.onChangeAccount}
type='account'
<div className='ui--grid'>
<div className='medium'>
<div className='ui--row'>
<InputAddress
className='full'
defaultValue={defaultPublicKey}
hideAddress
isInput={false}
label={t('editor.select', {
defaultValue: 'using my account'
})}
onChange={this.onChangeAccount}
type='account'
/>
</div>
<div className='ui--row'>
<div className='full'>
<Label>{t('editor.name', {
defaultValue: 'identified by the name'
})}</Label>
<Input
onChange={this.onChangeName}
value={editedName}
/>
</div>
</div>
</div>
<Address
className='medium'
value={{
address: currentPair.address(),
publicKey: currentPair.publicKey()
}}
/>
</div>
<div className='ui--form'>
<div className='medium'>
<Label>{t('editor.name', {
defaultValue: 'identified by the name'
})}</Label>
<Input
onChange={this.onChangeName}
value={editedName}
/>
</div>
</div>
<div className='ui--form-buttons'>
<div className='ui--row-buttons'>
<Button
disabled={!isEdited}
negative
onClick={this.onDiscard}
primary
>
{t('editor.reset', {
defaultValue: 'Reset'
@ -128,7 +140,9 @@ class Editor extends React.PureComponent<Props, State> {
onChangeAccount = (publicKey: Uint8Array): void => {
const currentPair = keyring.getPair(publicKey);
this.nextState({ currentPair });
this.nextState({
currentPair
});
}
// eslint-disable-next-line no-unused-vars

View File

@ -11,25 +11,29 @@
text-align: center;
}
.accounts--Icon {
position: fixed;
top: 0.75em;
right: 0.75em;
}
.accounts--Address {
opacity: 1 !important;
position: relative;
text-align: center;
}
.accounts--Address-icon {
position: absolute;
top: 2px;
left: 1rem;
display: block
}
.accounts--Address-name {
font-family: monospace;
padding-left: 3rem;
.accounts--Address-balance,
.accounts--Address-nonce {
opacity: 0.45;
}
.accounts--Address-data {}
.accounts--Address-data * {
margin: 0 0.25rem !important;
vertical-align: middle;
}
.accounts--Address-address {
display: inline-block;
font-family: monospace;
font-size: 1.5em;
padding: 0;
}

View File

@ -0,0 +1,15 @@
ISC License (ISC)
Copyright 2017-2018 Jaco Greeff <jacogr@gmail.com>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

View File

@ -0,0 +1 @@
# @polkadot/app-addresses

View File

@ -0,0 +1,17 @@
{
"name": "@polkadot/app-addresses",
"version": "0.3.3",
"main": "index.js",
"repository": "https://github.com/polkadot-js/apps.git",
"author": "Jaco Greeff <jacogr@gmail.com>",
"license": "ISC",
"scripts": {
"build": "polkadot-dev-build-babel",
"check": "stylelint 'src/**/*.css' && eslint src && flow check",
"test": "jest --coverage"
},
"devDependencies": {
"@polkadot/app-accounts": "^0.3.3",
"@polkadot/ui-react-app": "^0.3.3"
}
}

View File

@ -0,0 +1,186 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { I18nProps } from '@polkadot/ui-react-app/types';
import React from 'react';
import Button from 'semantic-ui-react/dist/es/elements/Button';
import Input from 'semantic-ui-react/dist/es/elements/Input';
import Label from 'semantic-ui-react/dist/es/elements/Label';
import keyring from '@polkadot/ui-keyring/src';
import addressDecode from '@polkadot/util-keyring/address/decode';
import addressEncode from '@polkadot/util-keyring/address/encode';
import Address from '@polkadot/app-accounts/src/Address';
import translate from './translate';
type Props = I18nProps & {
onBack: () => void
};
type State = {
address: string | null,
publicKey: Uint8Array | null,
fieldName: string,
isAddressValid: boolean,
isNameValid: boolean,
isValid: boolean,
name: string
}
class Creator extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = this.emptyState();
}
render (): React$Node {
const { className, style, t } = this.props;
const { address, fieldName, isAddressValid, isNameValid, isValid, name, publicKey } = this.state;
return (
<div
className={['addresses--Creator', className].join(' ')}
style={style}
>
<div className='ui--grid'>
<div className='medium'>
<div className='ui--row'>
<div className='full'>
<Label>{t('creator.address', {
defaultValue: 'add the following address'
})}</Label>
<Input
error={!isAddressValid}
name={`${fieldName}_address`}
onChange={this.onChangeAddress}
value={address}
/>
</div>
</div>
<div className='ui--row'>
<div className='full'>
<Label>{t('creator.name', {
defaultValue: 'name the entry'
})}</Label>
<Input
error={!isNameValid}
name={`${fieldName}_name`}
onChange={this.onChangeName}
value={name}
/>
</div>
</div>
</div>
<Address
className='medium'
value={
// flowlint-next-line sketchy-null-string:off
!address || !publicKey
? null
: {
address,
publicKey
}
}
/>
</div>
<div className='ui--row-buttons'>
<Button
onClick={this.onDiscard}
>
{t('creator.discard', {
defaultValue: 'Reset'
})}
</Button>
<Button
disabled={!isValid}
onClick={this.onCommit}
primary
>
{t('creator.save', {
defaultValue: 'Save'
})}
</Button>
</div>
</div>
);
}
emptyState (): State {
return {
address: '',
fieldName: `field_${Date.now()}`,
isAddressValid: false,
isNameValid: true,
isValid: false,
name: 'new address',
publicKey: null
};
}
nextState (newState: $Shape<State>): void {
this.setState(
(prevState: State, props: Props): $Shape<State> => {
const { address = prevState.address, name = prevState.name } = newState;
let nextAddress;
let publicKey;
try {
// $FlowFixMe we do expect failures... sometimes
publicKey = addressDecode(address);
nextAddress = addressEncode(publicKey);
} catch (error) {
nextAddress = void 0;
publicKey = void 0;
}
const isNameValid = !!name;
const isAddressValid = !!nextAddress;
return {
address: nextAddress,
isAddressValid,
isNameValid,
isValid: isAddressValid && isNameValid,
name,
publicKey
};
}
);
}
// eslint-disable-next-line no-unused-vars
onChangeAddress = (event: SyntheticEvent<*>, { value }): void => {
this.nextState({ address: value });
}
// eslint-disable-next-line no-unused-vars
onChangeName = (event: SyntheticEvent<*>, { value }): void => {
this.nextState({ name: value });
}
onCommit = (): void => {
const { onBack } = this.props;
const { address, name } = this.state;
// flowlint-next-line sketchy-null-string:off
if (address) {
keyring.saveAddress(address, { name });
}
onBack();
}
onDiscard = (): void => {
this.setState(this.emptyState());
}
}
export default translate(Creator);

View File

@ -0,0 +1,200 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { KeyringAddress } from '@polkadot/ui-keyring/types';
import type { I18nProps } from '@polkadot/ui-react-app/types';
import React from 'react';
import Button from 'semantic-ui-react/dist/es/elements/Button';
import Input from 'semantic-ui-react/dist/es/elements/Input';
import Label from 'semantic-ui-react/dist/es/elements/Label';
import keyring from '@polkadot/ui-keyring/src';
import InputAddress from '@polkadot/ui-react-app/src/InputAddress';
import Address from '@polkadot/app-accounts/src/Address';
import translate from './translate';
type Props = I18nProps & {
onBack: () => void
};
type State = {
currentAddress?: KeyringAddress,
defaultPublicKey?: Uint8Array,
editedName: string,
isEdited: boolean,
info: KeyringAddress | null
}
class Editor extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
const addresses = keyring.getAddresses();
const currentAddress = addresses[addresses.length - 1];
this.state = this.createState(currentAddress);
this.state.defaultPublicKey = currentAddress
? currentAddress.publicKey()
: void 0;
}
render (): React$Node {
const { className, style, t } = this.props;
const { currentAddress, defaultPublicKey, editedName, isEdited } = this.state;
if (!currentAddress) {
return (
<div
className={['addresses--Editor', className].join(' ')}
style={style}
>
{t('editor.none', {
defaultValue: 'There are no saved addresses. Add some first.'
})}
</div>
);
}
return (
<div
className={['addresses--Editor', className].join(' ')}
style={style}
>
<div className='ui--grid'>
<div className='medium'>
<div className='ui--row'>
<InputAddress
className='full'
defaultValue={defaultPublicKey}
hideAddress
isInput={false}
label={t('editor.select', {
defaultValue: 'edit the selected address'
})}
onChange={this.onChangeAddress}
type='address'
/>
</div>
<div className='ui--row'>
<div className='full'>
<Label>{t('editor.name', {
defaultValue: 'identified by the name'
})}</Label>
<Input
onChange={this.onChangeName}
value={editedName}
/>
</div>
</div>
</div>
<Address
className='medium'
value={{
address: currentAddress.address(),
publicKey: currentAddress.publicKey()
}}
/>
</div>
<div className='ui--row-buttons'>
<Button
disabled={!isEdited}
onClick={this.onDiscard}
>
{t('editor.reset', {
defaultValue: 'Reset'
})}
</Button>
<Button
disabled={!isEdited}
onClick={this.onCommit}
primary
>
{t('editor.save', {
defaultValue: 'Save'
})}
</Button>
</div>
</div>
);
}
createState (currentAddress?: KeyringAddress): $Shape<State> {
const { name = '' } = currentAddress
? currentAddress.getMeta()
: {};
return {
currentAddress,
editedName: name,
isEdited: false
};
}
nextState (newState?: $Shape<State> = {}): void {
this.setState(
(prevState: State): $Shape<State> => {
let { currentAddress = prevState.currentAddress, editedName = prevState.editedName } = newState;
const previous = prevState.currentAddress || { address: () => null };
if (currentAddress && currentAddress.address() !== previous.address()) {
editedName = currentAddress.getMeta().name || '';
}
const isEdited = !!currentAddress && editedName !== currentAddress.getMeta().name;
return {
currentAddress,
editedName,
isEdited
};
}
);
}
onChangeAddress = (publicKey: Uint8Array): void => {
const currentAddress = keyring.getAddress(publicKey);
this.nextState({
currentAddress
});
}
// eslint-disable-next-line no-unused-vars
onChangeName = (event: SyntheticEvent<*>, { value }): void => {
this.nextState({ editedName: value });
}
onCommit = (): void => {
const { currentAddress, editedName } = this.state;
if (!currentAddress) {
return;
}
keyring.saveAddress(currentAddress.address(), {
name: editedName,
whenEdited: Date.now()
});
this.nextState({});
}
onDiscard = (): void => {
const { currentAddress } = this.state;
if (!currentAddress) {
return;
}
this.nextState({
editedName: currentAddress.getMeta().name
});
}
}
export default translate(Editor);

View File

@ -0,0 +1,12 @@
/* Copyright 2017-2018 Jaco Greeff
/* This software may be modified and distributed under the terms
/* of the ISC license. See the LICENSE file for details. */
.addresses--App {
padding: 1em 2em;
}
.addresses--App-navigation {
margin-bottom: 2em;
text-align: center;
}

View File

@ -0,0 +1,95 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { I18nProps } from '@polkadot/ui-react-app/types';
import './index.css';
import React from 'react';
import Button from 'semantic-ui-react/dist/es/elements/Button';
import Creator from './Creator';
import Editor from './Editor';
import translate from './translate';
type Props = I18nProps & {};
type Actions = 'create' | 'edit';
type State = {
currentAction: Actions,
currentAddress: string | null
}
// FIXME React-router would probably be the best route, not home-grown
const Components: { [Actions]: React$ComponentType<*> } = {
'create': Creator,
'edit': Editor
};
class App extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
this.state = {
currentAction: 'edit',
currentAddress: null
};
}
render (): React$Node {
const { className, style, t } = this.props;
const { currentAction } = this.state;
const Component = Components[currentAction];
return (
<div
className={['addresses--App', className].join(' ')}
style={style}
>
<div className='addresses--App-navigation'>
<Button.Group>
<Button
onClick={this.selectEdit}
primary={currentAction === 'edit'}
>
{t('app.edit', {
defaultValue: 'Edit address'
})}
</Button>
<Button.Or
text={t('app.or', {
defaultValue: 'or'
})}
/>
<Button
onClick={this.selectCreate}
primary={currentAction === 'create'}
>
{t('app.create', {
defaultValue: 'Add address'
})}
</Button>
</Button.Group>
</div>
<Component onBack={this.selectEdit} />
</div>
);
}
setCurrentAction (currentAction: Actions): void {
this.setState({ currentAction });
}
selectCreate = (): void => {
this.setCurrentAction('create');
}
selectEdit = (): void => {
this.setCurrentAction('edit');
}
}
export default translate(App);

View File

@ -0,0 +1,8 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import { translate } from 'react-i18next';
export default translate(['addresses', 'ui']);

View File

@ -43,7 +43,7 @@ class Account extends React.PureComponent<Props, State> {
return (
<div
className={['extrinsics--Account', 'ui--form', className].join(' ')}
className={['extrinsics--Account', 'ui--row', className].join(' ')}
style={style}
>
<div className='large'>

View File

@ -47,19 +47,17 @@ export default class ExtrinsicDisplay extends React.PureComponent<Props, State>
return (
<div
className={['extrinsics--Extrinsic', 'ui--form', className].join(' ')}
className={['extrinsics--Extrinsic', className].join(' ')}
style={style}
>
<div className='full'>
<InputExtrinsic
defaultValue={defaultValue}
isError={isError}
isPrivate={isPrivate}
labelMethod={labelMethod}
labelSection={labelSection}
onChange={this.onChangeExtrinsic}
/>
</div>
<InputExtrinsic
defaultValue={defaultValue}
isError={isError}
isPrivate={isPrivate}
labelMethod={labelMethod}
labelSection={labelSection}
onChange={this.onChangeExtrinsic}
/>
<Params
item={extrinsic}
onChange={this.onChangeValues}

View File

@ -19,7 +19,7 @@ type Props = BareProps & {
export default function Nonce ({ className, label, style, onChange, value }: Props): React$Node {
return (
<div
className={['ui--form', className].join(' ')}
className={['ui--row', className].join(' ')}
style={style}
>
<div className='small'>

View File

@ -73,7 +73,7 @@ class Selection extends React.PureComponent<Props, State> {
onChange={this.onChangeNonce}
value={publicKey}
/>
<div className='ui--form-buttons'>
<div className='ui--row-buttons'>
<Button
disabled={!isValid}
onClick={this.onQueue}

View File

@ -67,7 +67,7 @@ class Unlock extends React.PureComponent<Props, State> {
className={['extrinsics--Signer-Unlock', className].join(' ')}
style={style}
>
<div className='expanded ui--form'>
<div className='expanded ui--row'>
<div className='medium'>
<Label>{t('unlock.password', {
defaultValue: 'unlock account using'

View File

@ -12,7 +12,7 @@
.storage--Query {
margin-bottom: 0.25em;
padding: 0.25em 0.75em;
padding: 0.25em 0;
}
.storage--Query .ui.disabled.dropdown.selection {
@ -24,6 +24,10 @@
margin-left: 0.5em;
}
.storage--Query .storage--actionrow-value {
padding: 0 0.25rem;
}
.storage--Query .ui--IdentityIcon {
margin: -10px 0;
vertical-align: middle;
@ -40,5 +44,5 @@
.storage--actionrow-button {
flex: 0;
padding-left: 0.5rem;
padding: 0 0.25rem;
}

View File

@ -13,6 +13,7 @@
"devDependencies": {
"@polkadot/ui-react-app": "^0.3.3",
"chalk": "^2.4.1",
"react-copy-to-clipboard": "^5.0.1",
"yargs": "10.1.2"
},
"optionalDependencies": {

View File

@ -6,6 +6,7 @@
import type { BareProps } from '@polkadot/ui-react-app/types';
import React from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import Button from 'semantic-ui-react/dist/es/elements/Button';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import u8aToHex from '@polkadot/util/u8a/toHex';
@ -21,6 +22,7 @@ type Props = BareProps & {
export default function Match ({ address, className, count, offset, onRemove, seed, style }: Props): React$Node {
const _onRemove = (): void =>
onRemove(address);
const hexSeed = u8aToHex(seed);
return (
<div
@ -33,18 +35,29 @@ export default function Match ({ address, className, count, offset, onRemove, se
size={48}
value={address}
/>
<div className='vanity--Match-addr'>
<span className='no'>{address.slice(0, offset)}</span><span className='yes'>{address.slice(offset, count + offset)}</span><span className='no'>{address.slice(count + offset)}</span>
<div className='vanity--Match-data'>
<div className='vanity--Match-addr'>
<span className='no'>{address.slice(0, offset)}</span><span className='yes'>{address.slice(offset, count + offset)}</span><span className='no'>{address.slice(count + offset)}</span>
</div>
<div className='vanity--Match-seed'>
{hexSeed}
</div>
</div>
<div className='vanity--Match-seed'>
{u8aToHex(seed)}
<div className='vanity--Match-buttons'>
<CopyToClipboard text={hexSeed}>
<Button
icon='copy'
primary
size='tiny'
/>
</CopyToClipboard>
<Button
icon='close'
negative
onClick={_onRemove}
size='tiny'
/>
</div>
<Button
className='vanity--Match-remove'
icon='close'
negative
onClick={_onRemove}
/>
</div>
</div>
);

View File

@ -34,7 +34,7 @@ let total = 0;
let indicator = -1;
if (!matchRegex.test(match)) {
console.error("Invalid character found in match string, allowed is '1-9' (no '0'), 'A-N & P-Z' (no 'O'), 'a-k & m-z' (no 'l') and '?' (wildcard)");
console.error("Invalid character found in match string, allowed is '1-9' (no '0'), 'A-H, J-N & P-Z' (no 'I' or 'O'), 'a-k & m-z' (no 'l') and '?' (wildcard)");
process.exit(-1);
}

View File

@ -3,7 +3,6 @@
// of the ISC license. See the LICENSE file for details.
// @flow
// NOTE bs58 characters, no '0', 'O' or 'l'
const regex = new RegExp('^[1-9A-NP-Za-km-z?]*$', '');
const regex = new RegExp('^[1-9A-HJ-NP-Za-km-z]*$', '');
module.exports = regex;

View File

@ -27,20 +27,21 @@
.vanity--Match-item {
display: inline-block;
font-family: monospace;
margin: 0.5em auto;
margin: 0 auto;
padding: 0.5em;
position: relative;
}
.vanity--Match-icon {
position: absolute;
top: 0;
left: 1rem;
.vanity--Match-icon,
.vanity--Match-data,
.vanity--Match-buttons {
display: inline-block;
vertical-align: middle;
}
.vanity--Match-addr,
.vanity--Match-seed {
padding: 0 5rem;
padding: 0 1rem;
}
.vanity--Match-addr {
@ -54,12 +55,6 @@
color: red;
}
.vanity--Match-remove {
position: absolute;
top: 0;
right: 1rem;
}
.vanity--Match-seed {
opacity: 0.45;
}

View File

@ -70,7 +70,7 @@ class App extends React.PureComponent<Props, State> {
className={['vanity--App', className].join(' ')}
style={style}
>
<div className='ui--form'>
<div className='ui--row'>
<div className='medium'>
<Label>{t('vanity.matching', {
defaultValue: 'generate address containing (? wildcard)'
@ -104,17 +104,16 @@ class App extends React.PureComponent<Props, State> {
</div>
</div>
</div>
<div className='ui--form-buttons'>
<div className='ui--row-buttons'>
<Button
disabled={!isMatchValid}
negative={isRunning}
primary
primary={!isRunning}
onClick={this.toggleStart}
>
{
isRunning
? t('vanity.stop', { defaultValue: 'Stop' })
: t('vanity.start', { defaultValue: 'Start' })
? t('vanity.stop', { defaultValue: 'Stop generation' })
: t('vanity.start', { defaultValue: 'Start generation' })
}
</Button>
</div>

View File

@ -15,6 +15,7 @@
},
"devDependencies": {
"@polkadot/app-accounts": "^0.3.3",
"@polkadot/app-addresses": "^0.3.3",
"@polkadot/app-explorer": "^0.3.3",
"@polkadot/app-extrinsics": "^0.3.3",
"@polkadot/app-storage": "^0.3.3",

View File

@ -13,7 +13,6 @@
.apps--Connecting-text {
background: red;
color: white;
display: inline-block;
line-height: 1.5em;
padding: 1em 2em;
}

View File

@ -29,7 +29,7 @@ function Connecting ({ className, style, value, t }: Props): React$Node {
>
<div className='apps--Connecting-text'>
{t('connecting.disconnected', {
defaultValue: 'You are disconnected from the node. Check that your node is running and that the Websocket endpoint is reachable.'
defaultValue: 'You are not connected to a node. Ensure that your node is running and that the Websocket endpoint is reachable.'
})}
</div>
</div>

View File

@ -0,0 +1,21 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { Routes } from '../types';
import Addresses from '@polkadot/app-addresses/src';
export default ([
{
component: Addresses,
i18n: {
defaultValue: 'Addresses'
},
icon: 'address book',
isExact: false,
isHidden: false,
name: 'addresses'
}
]: Routes);

View File

@ -6,7 +6,7 @@
import type { Routing } from '../types';
import accounts from './accounts';
// import addresses from './addresses';
import addresses from './addresses';
import explorer from './explorer';
import extrinsics from './extrinsics';
// import home from './home';
@ -17,6 +17,6 @@ import vanitygen from './vanitygen';
export default ({
default: 'explorer',
routes: [].concat(
explorer, extrinsics, storage, null, accounts, vanitygen
explorer, extrinsics, storage, null, accounts, addresses, vanitygen
)
}: Routing);

View File

@ -6,7 +6,7 @@
import type { KeyringPair, KeyringPair$Meta } from '@polkadot/util-keyring/types';
import type { State } from '../types';
import saveAccount from './account';
import saveAccount from './save';
export default function accountCreate (state: State, seed: Uint8Array, password?: string, meta?: KeyringPair$Meta = {}): KeyringPair {
const pair = state.keyring.addFromSeed(seed, meta);

View File

@ -0,0 +1,16 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { KeyringAddress, State } from '../types';
import get from './get';
export default function all (state: State): Array<KeyringAddress> {
return Object
.keys(state.available.address)
.map((address) =>
get(state, address)
);
}

View File

@ -0,0 +1,27 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { KeyringAddress, KeyringJson$Meta, State } from '../types';
import isString from '@polkadot/util/is/string';
import addressDecode from '@polkadot/util-keyring/address/decode';
import addressEncode from '@polkadot/util-keyring/address/encode';
export default function get (state: State, _address: string | Uint8Array): KeyringAddress {
const address = isString(_address)
// flowlint-next-line unclear-type:off
? ((_address: any): string)
: addressEncode(_address);
const publicKey = addressDecode(address);
return {
address: (): string =>
address,
publicKey: (): Uint8Array =>
publicKey,
getMeta: (): KeyringJson$Meta =>
state.available.address[address].meta
};
}

View File

@ -0,0 +1,33 @@
// Copyright 2017-2018 Jaco Greeff
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// @flow
import type { KeyringPair$Meta } from '@polkadot/util-keyring/types';
import type { KeyringJson, State } from '../types';
import store from 'store';
import { addressKey } from '../defaults';
import createOptions from '../options';
export default function saveMeta (state: State, address: string, meta: KeyringPair$Meta): void {
const json = state.available[address] || {
address,
meta: {
isRecent: void 0,
whenCreated: Date.now()
}
};
Object.keys(meta).forEach((key) => {
json.meta[key] = meta[key];
});
delete json.meta.isRecent;
store.set(addressKey(address), json);
state.available.address[address] = (json: $Shape<KeyringJson>);
createOptions(state);
}

View File

@ -3,7 +3,6 @@
// of the ISC license. See the LICENSE file for details.
// @flow
// import type { KeyringPair$Meta } from '@polkadot/util-keyring/types';
import type { KeyringJson, KeyringOption, State } from '../types';
import store from 'store';

View File

@ -4,15 +4,18 @@
// @flow
import type { KeyringPair, KeyringPair$Meta } from '@polkadot/util-keyring/types';
import type { KeyringInstance, KeyringOption$Type, KeyringOption, KeyringOptions, State } from './types';
import type { KeyringAddress, KeyringInstance, KeyringOption$Type, KeyringOption, KeyringOptions, State } from './types';
import testKeyring from '@polkadot/util-keyring/testing';
import loadAll from './loadAll';
import createAccount from './save/accountCreate';
import saveAccount from './save/account';
import saveAccountMeta from './save/accountMeta';
import saveRecent from './save/recent';
import createAccount from './account/create';
import saveAccount from './account/save';
import saveAccountMeta from './account/meta';
import getAddress from './address/get';
import getAddresses from './address/all';
import saveAddress from './address/meta';
import saveRecent from './address/metaRecent';
const state: State = {
available: {
@ -26,20 +29,26 @@ const state: State = {
loadAll(state);
export default ({
createAccount: (seed: Uint8Array, password?: string, meta?: KeyringPair$Meta): KeyringPair =>
createAccount(state, seed, password, meta),
getAddress: (address: string | Uint8Array): KeyringAddress =>
getAddress(state, address),
getAddresses: (): Array<KeyringAddress> =>
getAddresses(state),
getOptions: (type: KeyringOption$Type): KeyringOptions =>
state.options[type],
getPair: (address: string | Uint8Array): KeyringPair =>
state.keyring.getPair(address),
getPairs: (): Array<KeyringPair> =>
state.keyring.getPairs(),
getOptions: (type: KeyringOption$Type): KeyringOptions =>
state.options[type],
loadAll: (): void =>
loadAll(state),
createAccount: (seed: Uint8Array, password?: string, meta?: KeyringPair$Meta): KeyringPair =>
createAccount(state, seed, password, meta),
saveAccount: (pair: KeyringPair, password?: string): void =>
saveAccount(state, pair, password),
saveAccountMeta: (pair: KeyringPair, meta: KeyringPair$Meta): void =>
saveAccountMeta(state, pair, meta),
saveAddress: (address: string, meta: KeyringPair$Meta): void =>
saveAddress(state, address, meta),
saveRecent: (address: string): KeyringOption =>
saveRecent(state, address)
}: KeyringInstance);

View File

@ -25,18 +25,26 @@ export type KeyringOption = KeyringOption$Data & {
export type KeyringOptions = Array<KeyringOption>;
export type KeyringJson$Meta = {
isRecent?: boolean,
isTesting?: boolean,
name?: string,
whenCreated?: number,
whenEdited?: number,
whenUsed?: number
};
export type KeyringJson = {
address: string,
meta: {
isRecent?: boolean,
isTesting?: boolean,
name?: string,
whenCreated?: number,
whenEdited?: number,
whenUsed?: number
}
meta: KeyringJson$Meta
};
export type KeyringAddress = {
address: () => string,
publicKey: () => Uint8Array,
getMeta: () => KeyringJson$Meta
}
export type State = {
available: {
account: { [string]: KeyringJson },
@ -49,12 +57,15 @@ export type State = {
};
export type KeyringInstance = {
createAccount: (seed: Uint8Array, password?: string, meta?: KeyringPair$Meta) => KeyringPair,
getAddress: (address: string | Uint8Array) => KeyringAddress,
getAddresses: () => Array<KeyringAddress>,
getOptions: (type: KeyringOption$Type) => KeyringOptions,
getPair: (address: string | Uint8Array) => KeyringPair,
getPairs: () => Array<KeyringPair>,
loadAll: () => void,
createAccount: (seed: Uint8Array, password?: string, meta?: KeyringPair$Meta) => KeyringPair,
saveAccount: (pair: KeyringPair, password?: string) => void,
saveAccountMeta: (pair: KeyringPair, meta: KeyringPair$Meta) => void,
saveAddress: (address: string, meta: KeyringPair$Meta) => void,
saveRecent: (address: string) => KeyringOption
};

View File

@ -4,10 +4,15 @@
.ui--InputAddress {}
.ui--InputAddress > .text {
.ui--InputAddress .ui.dropdown .text {
width: 100%;
}
.ui--InputAddress input.search {
margin-left: 3rem;
}
.flag--hideAddress .ui--KeyPair .ui--KeyPair-address {
flex: 0;
max-width: 0;
}

View File

@ -17,6 +17,7 @@ import RxDropdown from '../RxDropdown';
type Props = BareProps & {
defaultValue?: Uint8Array,
hideAddress?: boolean;
isError?: boolean,
isInput?: boolean,
label?: string,
@ -57,13 +58,13 @@ export default class InputAddress extends React.Component<Props, State> {
}
render (): React$Node {
const { className, isError, label, onChange, style, type = 'all' } = this.props;
const { className, hideAddress = false, isError, label, onChange, style, type = 'all' } = this.props;
const { defaultValue } = this.state;
const options = keyring.getOptions(type);
return (
<RxDropdown
className={['ui--InputAddress', className].join(' ')}
className={['ui--InputAddress', hideAddress ? 'flag--hideAddress' : '', className].join(' ')}
defaultValue={defaultValue}
isError={isError}
label={label}

View File

@ -49,7 +49,7 @@ class InputExtrinsic extends React.PureComponent<Props, State> {
return (
<div
className={['ui--RxDropdownLinked', 'ui--form', className].join(' ')}
className={['ui--RxDropdownLinked', 'ui--row', className].join(' ')}
style={style}
>
<div className='small'>

View File

@ -47,7 +47,7 @@ class InputStorage extends React.PureComponent<Props, State> {
return (
<div
className={['ui--RxDropdownLinked', 'ui--form', className].join(' ')}
className={['ui--RxDropdownLinked', 'ui--row', className].join(' ')}
style={style}
>
<div className='small'>

View File

@ -18,7 +18,7 @@ type Props = BareProps & {
export default function Base ({ children, className, label, size = 'medium', style }: Props): React$Node {
return (
<div
className={['ui--form', className].join(' ')}
className={['ui--row', className].join(' ')}
style={style}
>
<div className={size}>

View File

@ -69,7 +69,7 @@ class Params extends React.PureComponent<Props, State> {
return (
<div
className={['ui--Params', 'ui--form', className].join(' ')}
className={['ui--Params', className].join(' ')}
style={style}
>
<div className='ui--Params-Content'>

View File

@ -34,17 +34,23 @@ export default function RxDropdown (props: Props): React$Node {
delete _props.onChange;
delete _props.transform;
return [
<Label key='label'>
{props.label || ''}
</Label>,
<Dropdown
key='dropdown'
selection
{..._props}
className={props.className}
error={props.isError}
onChange={_onChange}
/>
];
return (
<div
className={['ui--Dropdown', props.className].join(' ')}
style={props.style}
>
<Label key='label'>
{props.label || ''}
</Label>,
<Dropdown
key='dropdown'
selection
{..._props}
className={null}
error={props.isError}
onChange={_onChange}
style={{}}
/>
</div>
);
}

View File

@ -2,11 +2,13 @@
/* This software may be modified and distributed under the terms
/* of the ISC license. See the LICENSE file for details. */
.ui--form {
.ui--grid,
.ui--row {
width: 100%;
}
.ui--form {
.ui--grid,
.ui--row {
display: flex;
flex-wrap: wrap;
flex-direction: row;
@ -14,45 +16,59 @@
align-items: stretch;
}
.ui--form.embedded {
margin-right: -0.5rem;
}
.ui--form > div {
.ui--grid > div,
.ui--row > div {
box-sizing: border-box;
min-width: 0;
width: 100%;
}
.ui--form > div.full:not(:first-child),
.ui--form > div.large:not(:first-child),
.ui--form > div.medium:not(:first-child),
.ui--form > div.small:not(:first-child) {
padding-left: 0.5rem;
}
.ui--form > div.full {
.ui--grid > div,
.ui--grid > div.full,
.ui--row > div.full {
flex: 0 100%;
}
.ui--form > div.large {
.ui--grid > div.large,
.ui--row > div.large {
flex: 0 75%;
}
.ui--form > div.medium {
.ui--grid > div.medium,
.ui--row > div.medium {
flex: 0 50%;
}
.ui--form > div.small {
.ui--grid > div.small,
.ui--row > div.small {
flex: 0 25%;
}
.ui--form-buttons {
margin: 1em 0;
text-align: right;
.ui--grid > div.sixty6,
.ui--row > div.sixty6 {
flex: 0 66.66%;
}
.ui--form-buttons .button {
margin: 0 0 0 0.5rem;
.ui--grid > div.thirty3,
.ui--row > div.thirty3 {
flex: 0 33.33%;
}
.ui--row > div.full,
.ui--row > div.large,
.ui--row > div.medium,
.ui--row > div.small,
.ui--row > div.sixty6,
.ui--row > div.thirty3 {
padding: 0 0.25rem;
}
.ui--row-buttons {
margin: 1em 0;
text-align: center;
}
.ui--row-buttons .button {
margin: 0 0.25rem;
text-align: left;
}

View File

@ -40,6 +40,17 @@
z-index: 1;
}
.ui.modal > .actions {
background: transparent;
border-top: none;
text-align: center;
}
.ui.modal > .actions .button {
margin: 0 0.25rem;
text-align: left;
}
.ui.page.modals.transition.visible {
display: flex !important;
}

View File

@ -18,7 +18,7 @@ export type I18nProps = BareProps & {
export type BaseContext = {
api: RxApiInterface,
// TODO: Set the correct/full type
// TODO: Set the correct type
router: {
route: {
location: Location

View File

@ -3558,6 +3558,12 @@ copy-descriptor@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
copy-to-clipboard@^3:
version "3.0.8"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9"
dependencies:
toggle-selection "^1.0.3"
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@ -8631,7 +8637,7 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.6.0:
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0:
version "15.6.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
dependencies:
@ -8783,6 +8789,13 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-copy-to-clipboard@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
dependencies:
copy-to-clipboard "^3"
prop-types "^15.5.8"
react-dev-utils@6.0.0-next.66cc7a90:
version "6.0.0-next.66cc7a90"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-6.0.0-next.66cc7a90.tgz#9d5fb27615454a94b448c679e4a79ea1fe716fba"
@ -10452,6 +10465,10 @@ to-regex@^3.0.1, to-regex@^3.0.2:
regex-not "^1.0.2"
safe-regex "^1.1.0"
toggle-selection@^1.0.3:
version "1.0.6"
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
toposort@^1.0.0:
version "1.0.7"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"