From 0eab4f6f22e28f7970f5729220e1d07818ec7ef6 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 11 May 2018 09:20:44 +0200 Subject: [PATCH] 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 --- flow-typed/react-copy-to-clipboard.js | 5 + packages/app-accounts/src/Address.js | 64 ++++-- packages/app-accounts/src/Creator.js | 137 ++++++------ packages/app-accounts/src/Editor.js | 64 +++--- packages/app-accounts/src/index.css | 32 +-- packages/app-addresses/LICENSE | 15 ++ packages/app-addresses/README.md | 1 + packages/app-addresses/package.json | 17 ++ packages/app-addresses/src/Creator.js | 186 ++++++++++++++++ packages/app-addresses/src/Editor.js | 200 ++++++++++++++++++ packages/app-addresses/src/index.css | 12 ++ packages/app-addresses/src/index.js | 95 +++++++++ packages/app-addresses/src/translate.js | 8 + packages/app-extrinsics/src/Account.js | 2 +- packages/app-extrinsics/src/Extrinsic.js | 20 +- packages/app-extrinsics/src/Nonce.js | 2 +- packages/app-extrinsics/src/Selection.js | 2 +- packages/app-extrinsics/src/Signer/Unlock.js | 2 +- packages/app-storage/src/index.css | 8 +- packages/app-vanitygen/package.json | 1 + packages/app-vanitygen/src/Match.js | 33 ++- packages/app-vanitygen/src/generator/cli.js | 2 +- packages/app-vanitygen/src/generator/regex.js | 3 +- packages/app-vanitygen/src/index.css | 19 +- packages/app-vanitygen/src/index.js | 11 +- packages/apps/package.json | 1 + packages/apps/src/Connecting/Connecting.css | 1 - packages/apps/src/Connecting/index.js | 2 +- packages/apps/src/routing/addresses.js | 21 ++ packages/apps/src/routing/index.js | 4 +- .../accountCreate.js => account/create.js} | 2 +- .../{save/accountMeta.js => account/meta.js} | 0 .../src/{save/account.js => account/save.js} | 0 packages/ui-keyring/src/address/all.js | 16 ++ packages/ui-keyring/src/address/get.js | 27 +++ packages/ui-keyring/src/address/meta.js | 33 +++ .../{save/recent.js => address/metaRecent.js} | 1 - packages/ui-keyring/src/index.js | 27 ++- packages/ui-keyring/src/types.js | 29 ++- .../src/InputAddress/InputAddress.css | 7 +- .../ui-react-app/src/InputAddress/index.js | 5 +- .../ui-react-app/src/InputExtrinsic/index.js | 2 +- .../ui-react-app/src/InputStorage/index.js | 2 +- .../ui-react-app/src/Params/Param/Base.js | 2 +- packages/ui-react-app/src/Params/index.js | 2 +- packages/ui-react-app/src/RxDropdown.js | 32 +-- packages/ui-react-app/src/styles/form.css | 62 ++++-- packages/ui-react-app/src/styles/semantic.css | 11 + packages/ui-react-app/src/types.js | 2 +- yarn.lock | 19 +- 50 files changed, 1016 insertions(+), 235 deletions(-) create mode 100644 flow-typed/react-copy-to-clipboard.js create mode 100644 packages/app-addresses/LICENSE create mode 100644 packages/app-addresses/README.md create mode 100644 packages/app-addresses/package.json create mode 100644 packages/app-addresses/src/Creator.js create mode 100644 packages/app-addresses/src/Editor.js create mode 100644 packages/app-addresses/src/index.css create mode 100644 packages/app-addresses/src/index.js create mode 100644 packages/app-addresses/src/translate.js create mode 100644 packages/apps/src/routing/addresses.js rename packages/ui-keyring/src/{save/accountCreate.js => account/create.js} (93%) rename packages/ui-keyring/src/{save/accountMeta.js => account/meta.js} (100%) rename packages/ui-keyring/src/{save/account.js => account/save.js} (100%) create mode 100644 packages/ui-keyring/src/address/all.js create mode 100644 packages/ui-keyring/src/address/get.js create mode 100644 packages/ui-keyring/src/address/meta.js rename packages/ui-keyring/src/{save/recent.js => address/metaRecent.js} (91%) diff --git a/flow-typed/react-copy-to-clipboard.js b/flow-typed/react-copy-to-clipboard.js new file mode 100644 index 0000000000..80efcc19c4 --- /dev/null +++ b/flow-typed/react-copy-to-clipboard.js @@ -0,0 +1,5 @@ +// @flow + +declare module 'react-copy-to-clipboard' { + declare module.exports: React$ComponentType<{ text: string }>; +} diff --git a/packages/app-accounts/src/Address.js b/packages/app-accounts/src/Address.js index 6aa0da6e78..c689b983c0 100644 --- a/packages/app-accounts/src/Address.js +++ b/packages/app-accounts/src/Address.js @@ -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 (
- { - // flowlint-next-line sketchy-null-string:off - address - ? ( - - ) - : null - } -
- { - // flowlint-next-line sketchy-null-string:off - address || 'unknown' - } + +
+
+ {short} +
+ +
+ + + {' transactions'} +
); } diff --git a/packages/app-accounts/src/Creator.js b/packages/app-accounts/src/Creator.js index c7b0827660..4de3b58c22 100644 --- a/packages/app-accounts/src/Creator.js +++ b/packages/app-accounts/src/Creator.js @@ -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 { @@ -61,72 +66,80 @@ class Creator extends React.PureComponent { 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 (
-
+
- - -
-
- -
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + + +
+
+
-
-
- - -
-
-
-
- - - -
-
-
+
+ +
+
+ ); + } + + emptyState (): State { + return { + address: '', + fieldName: `field_${Date.now()}`, + isAddressValid: false, + isNameValid: true, + isValid: false, + name: 'new address', + publicKey: null + }; + } + + nextState (newState: $Shape): void { + this.setState( + (prevState: State, props: Props): $Shape => { + 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); diff --git a/packages/app-addresses/src/Editor.js b/packages/app-addresses/src/Editor.js new file mode 100644 index 0000000000..a48143aaf6 --- /dev/null +++ b/packages/app-addresses/src/Editor.js @@ -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 { + 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 ( +
+ {t('editor.none', { + defaultValue: 'There are no saved addresses. Add some first.' + })} +
+ ); + } + + return ( +
+
+
+
+ +
+
+
+ + +
+
+
+
+
+
+ + +
+
+ ); + } + + createState (currentAddress?: KeyringAddress): $Shape { + const { name = '' } = currentAddress + ? currentAddress.getMeta() + : {}; + + return { + currentAddress, + editedName: name, + isEdited: false + }; + } + + nextState (newState?: $Shape = {}): void { + this.setState( + (prevState: State): $Shape => { + 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); diff --git a/packages/app-addresses/src/index.css b/packages/app-addresses/src/index.css new file mode 100644 index 0000000000..d94d4ee657 --- /dev/null +++ b/packages/app-addresses/src/index.css @@ -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; +} diff --git a/packages/app-addresses/src/index.js b/packages/app-addresses/src/index.js new file mode 100644 index 0000000000..922c557c35 --- /dev/null +++ b/packages/app-addresses/src/index.js @@ -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 { + 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 ( +
+
+ + + + + +
+ +
+ ); + } + + setCurrentAction (currentAction: Actions): void { + this.setState({ currentAction }); + } + + selectCreate = (): void => { + this.setCurrentAction('create'); + } + + selectEdit = (): void => { + this.setCurrentAction('edit'); + } +} + +export default translate(App); diff --git a/packages/app-addresses/src/translate.js b/packages/app-addresses/src/translate.js new file mode 100644 index 0000000000..a74efd3543 --- /dev/null +++ b/packages/app-addresses/src/translate.js @@ -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']); diff --git a/packages/app-extrinsics/src/Account.js b/packages/app-extrinsics/src/Account.js index 1fa8b418fa..cd5679e0f0 100644 --- a/packages/app-extrinsics/src/Account.js +++ b/packages/app-extrinsics/src/Account.js @@ -43,7 +43,7 @@ class Account extends React.PureComponent { return (
diff --git a/packages/app-extrinsics/src/Extrinsic.js b/packages/app-extrinsics/src/Extrinsic.js index a29b1c2b9f..a626519c6a 100644 --- a/packages/app-extrinsics/src/Extrinsic.js +++ b/packages/app-extrinsics/src/Extrinsic.js @@ -47,19 +47,17 @@ export default class ExtrinsicDisplay extends React.PureComponent return (
-
- -
+
diff --git a/packages/app-extrinsics/src/Selection.js b/packages/app-extrinsics/src/Selection.js index 15d45bcd80..a805c94abf 100644 --- a/packages/app-extrinsics/src/Selection.js +++ b/packages/app-extrinsics/src/Selection.js @@ -73,7 +73,7 @@ class Selection extends React.PureComponent { onChange={this.onChangeNonce} value={publicKey} /> -
+
-
); diff --git a/packages/app-vanitygen/src/generator/cli.js b/packages/app-vanitygen/src/generator/cli.js index 76742907f0..28e6998eee 100644 --- a/packages/app-vanitygen/src/generator/cli.js +++ b/packages/app-vanitygen/src/generator/cli.js @@ -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); } diff --git a/packages/app-vanitygen/src/generator/regex.js b/packages/app-vanitygen/src/generator/regex.js index da43e650b3..98370f3aac 100644 --- a/packages/app-vanitygen/src/generator/regex.js +++ b/packages/app-vanitygen/src/generator/regex.js @@ -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; diff --git a/packages/app-vanitygen/src/index.css b/packages/app-vanitygen/src/index.css index 220e4f5242..ae208d93bc 100644 --- a/packages/app-vanitygen/src/index.css +++ b/packages/app-vanitygen/src/index.css @@ -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; } diff --git a/packages/app-vanitygen/src/index.js b/packages/app-vanitygen/src/index.js index 228e3155d4..8e6e655ece 100644 --- a/packages/app-vanitygen/src/index.js +++ b/packages/app-vanitygen/src/index.js @@ -70,7 +70,7 @@ class App extends React.PureComponent { className={['vanity--App', className].join(' ')} style={style} > -
+
-
+
diff --git a/packages/apps/package.json b/packages/apps/package.json index 71d997f06c..7992ff2f6b 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -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", diff --git a/packages/apps/src/Connecting/Connecting.css b/packages/apps/src/Connecting/Connecting.css index 53930f7376..4c41db8476 100644 --- a/packages/apps/src/Connecting/Connecting.css +++ b/packages/apps/src/Connecting/Connecting.css @@ -13,7 +13,6 @@ .apps--Connecting-text { background: red; color: white; - display: inline-block; line-height: 1.5em; padding: 1em 2em; } diff --git a/packages/apps/src/Connecting/index.js b/packages/apps/src/Connecting/index.js index 8b19037244..fa1b97e9d9 100644 --- a/packages/apps/src/Connecting/index.js +++ b/packages/apps/src/Connecting/index.js @@ -29,7 +29,7 @@ function Connecting ({ className, style, value, t }: Props): React$Node { >
{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.' })}
diff --git a/packages/apps/src/routing/addresses.js b/packages/apps/src/routing/addresses.js new file mode 100644 index 0000000000..6ac7c108bb --- /dev/null +++ b/packages/apps/src/routing/addresses.js @@ -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); diff --git a/packages/apps/src/routing/index.js b/packages/apps/src/routing/index.js index 1e4025881a..1861bd57e9 100644 --- a/packages/apps/src/routing/index.js +++ b/packages/apps/src/routing/index.js @@ -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); diff --git a/packages/ui-keyring/src/save/accountCreate.js b/packages/ui-keyring/src/account/create.js similarity index 93% rename from packages/ui-keyring/src/save/accountCreate.js rename to packages/ui-keyring/src/account/create.js index fbe2c9d318..b6fe3834e2 100644 --- a/packages/ui-keyring/src/save/accountCreate.js +++ b/packages/ui-keyring/src/account/create.js @@ -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); diff --git a/packages/ui-keyring/src/save/accountMeta.js b/packages/ui-keyring/src/account/meta.js similarity index 100% rename from packages/ui-keyring/src/save/accountMeta.js rename to packages/ui-keyring/src/account/meta.js diff --git a/packages/ui-keyring/src/save/account.js b/packages/ui-keyring/src/account/save.js similarity index 100% rename from packages/ui-keyring/src/save/account.js rename to packages/ui-keyring/src/account/save.js diff --git a/packages/ui-keyring/src/address/all.js b/packages/ui-keyring/src/address/all.js new file mode 100644 index 0000000000..8e344b8e14 --- /dev/null +++ b/packages/ui-keyring/src/address/all.js @@ -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 { + return Object + .keys(state.available.address) + .map((address) => + get(state, address) + ); +} diff --git a/packages/ui-keyring/src/address/get.js b/packages/ui-keyring/src/address/get.js new file mode 100644 index 0000000000..22e0a695ba --- /dev/null +++ b/packages/ui-keyring/src/address/get.js @@ -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 + }; +} diff --git a/packages/ui-keyring/src/address/meta.js b/packages/ui-keyring/src/address/meta.js new file mode 100644 index 0000000000..749f29bef8 --- /dev/null +++ b/packages/ui-keyring/src/address/meta.js @@ -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); + + createOptions(state); +} diff --git a/packages/ui-keyring/src/save/recent.js b/packages/ui-keyring/src/address/metaRecent.js similarity index 91% rename from packages/ui-keyring/src/save/recent.js rename to packages/ui-keyring/src/address/metaRecent.js index 2fac9e19c6..021c0d229a 100644 --- a/packages/ui-keyring/src/save/recent.js +++ b/packages/ui-keyring/src/address/metaRecent.js @@ -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'; diff --git a/packages/ui-keyring/src/index.js b/packages/ui-keyring/src/index.js index cd6ae6211e..7662b00634 100644 --- a/packages/ui-keyring/src/index.js +++ b/packages/ui-keyring/src/index.js @@ -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 => + getAddresses(state), + getOptions: (type: KeyringOption$Type): KeyringOptions => + state.options[type], getPair: (address: string | Uint8Array): KeyringPair => state.keyring.getPair(address), getPairs: (): Array => 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); diff --git a/packages/ui-keyring/src/types.js b/packages/ui-keyring/src/types.js index 0fbab40710..9c7619d5da 100644 --- a/packages/ui-keyring/src/types.js +++ b/packages/ui-keyring/src/types.js @@ -25,18 +25,26 @@ export type KeyringOption = KeyringOption$Data & { export type KeyringOptions = Array; +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, getOptions: (type: KeyringOption$Type) => KeyringOptions, getPair: (address: string | Uint8Array) => KeyringPair, getPairs: () => Array, 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 }; diff --git a/packages/ui-react-app/src/InputAddress/InputAddress.css b/packages/ui-react-app/src/InputAddress/InputAddress.css index f90478a351..e1cb682b8a 100644 --- a/packages/ui-react-app/src/InputAddress/InputAddress.css +++ b/packages/ui-react-app/src/InputAddress/InputAddress.css @@ -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; +} diff --git a/packages/ui-react-app/src/InputAddress/index.js b/packages/ui-react-app/src/InputAddress/index.js index 8055a05356..28a0502e17 100644 --- a/packages/ui-react-app/src/InputAddress/index.js +++ b/packages/ui-react-app/src/InputAddress/index.js @@ -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 { } 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 ( { return (
diff --git a/packages/ui-react-app/src/InputStorage/index.js b/packages/ui-react-app/src/InputStorage/index.js index 7731ff97a1..07a9291211 100644 --- a/packages/ui-react-app/src/InputStorage/index.js +++ b/packages/ui-react-app/src/InputStorage/index.js @@ -47,7 +47,7 @@ class InputStorage extends React.PureComponent { return (
diff --git a/packages/ui-react-app/src/Params/Param/Base.js b/packages/ui-react-app/src/Params/Param/Base.js index 2adbe7b916..6d6ce40b38 100644 --- a/packages/ui-react-app/src/Params/Param/Base.js +++ b/packages/ui-react-app/src/Params/Param/Base.js @@ -18,7 +18,7 @@ type Props = BareProps & { export default function Base ({ children, className, label, size = 'medium', style }: Props): React$Node { return (
diff --git a/packages/ui-react-app/src/Params/index.js b/packages/ui-react-app/src/Params/index.js index 0d859222db..a47f7c2162 100644 --- a/packages/ui-react-app/src/Params/index.js +++ b/packages/ui-react-app/src/Params/index.js @@ -69,7 +69,7 @@ class Params extends React.PureComponent { return (
diff --git a/packages/ui-react-app/src/RxDropdown.js b/packages/ui-react-app/src/RxDropdown.js index 2a43aa63c0..7455719050 100644 --- a/packages/ui-react-app/src/RxDropdown.js +++ b/packages/ui-react-app/src/RxDropdown.js @@ -34,17 +34,23 @@ export default function RxDropdown (props: Props): React$Node { delete _props.onChange; delete _props.transform; - return [ - , - - ]; + return ( +
+ , + +
+ ); } diff --git a/packages/ui-react-app/src/styles/form.css b/packages/ui-react-app/src/styles/form.css index 21b5351353..1b14de1147 100644 --- a/packages/ui-react-app/src/styles/form.css +++ b/packages/ui-react-app/src/styles/form.css @@ -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; } diff --git a/packages/ui-react-app/src/styles/semantic.css b/packages/ui-react-app/src/styles/semantic.css index 0fa9d0fda3..64a1780e15 100644 --- a/packages/ui-react-app/src/styles/semantic.css +++ b/packages/ui-react-app/src/styles/semantic.css @@ -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; } diff --git a/packages/ui-react-app/src/types.js b/packages/ui-react-app/src/types.js index 8de569b892..1c2ae96de9 100644 --- a/packages/ui-react-app/src/types.js +++ b/packages/ui-react-app/src/types.js @@ -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 diff --git a/yarn.lock b/yarn.lock index d7f19e8d51..570d394437 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"