From a4b0c494d334a8d7c8592db2e59dd2d221c20750 Mon Sep 17 00:00:00 2001 From: kwingram25 Date: Thu, 15 Aug 2019 22:40:27 +0000 Subject: [PATCH] Claim tokens app (#1485) * Ethereum pk -> address * [WIP] Claim tokens app * cleanup * refactor * Fixup address extraction * Use card instead of Inset (UI alignment) * Tadaaa! Claiming works. * Uppercase token name * Claim location (& overall spacing) * Remove stray console.error * Some documentation around utils --- packages/app-claims/LICENSE | 201 ++++++++++++++++ packages/app-claims/README.md | 1 + packages/app-claims/package.json | 17 ++ packages/app-claims/src/.test-jaco.json | 6 + packages/app-claims/src/.test-keith.json | 6 + packages/app-claims/src/Claim.tsx | 152 ++++++++++++ packages/app-claims/src/index.tsx | 234 +++++++++++++++++++ packages/app-claims/src/secp256k1.d.ts | 8 + packages/app-claims/src/translate.ts | 7 + packages/app-claims/src/util.spec.ts | 19 ++ packages/app-claims/src/util.ts | 111 +++++++++ packages/apps-routing/src/claims.ts | 23 ++ packages/apps-routing/src/index.ts | 12 +- packages/react-components/src/Card.tsx | 30 ++- packages/react-components/src/Collection.tsx | 13 +- packages/react-components/src/Inset.tsx | 23 +- packages/react-components/src/TxModal.tsx | 8 +- packages/react-components/src/styles/app.css | 5 + packages/react-signer/src/Modal.tsx | 2 +- tsconfig.json | 2 + 20 files changed, 868 insertions(+), 12 deletions(-) create mode 100644 packages/app-claims/LICENSE create mode 100644 packages/app-claims/README.md create mode 100644 packages/app-claims/package.json create mode 100644 packages/app-claims/src/.test-jaco.json create mode 100644 packages/app-claims/src/.test-keith.json create mode 100644 packages/app-claims/src/Claim.tsx create mode 100644 packages/app-claims/src/index.tsx create mode 100644 packages/app-claims/src/secp256k1.d.ts create mode 100644 packages/app-claims/src/translate.ts create mode 100644 packages/app-claims/src/util.spec.ts create mode 100644 packages/app-claims/src/util.ts create mode 100644 packages/apps-routing/src/claims.ts diff --git a/packages/app-claims/LICENSE b/packages/app-claims/LICENSE new file mode 100644 index 0000000000..0d381b2e97 --- /dev/null +++ b/packages/app-claims/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/app-claims/README.md b/packages/app-claims/README.md new file mode 100644 index 0000000000..0da50d6462 --- /dev/null +++ b/packages/app-claims/README.md @@ -0,0 +1 @@ +# @polkadot/app-claims diff --git a/packages/app-claims/package.json b/packages/app-claims/package.json new file mode 100644 index 0000000000..27d4b377c0 --- /dev/null +++ b/packages/app-claims/package.json @@ -0,0 +1,17 @@ +{ + "name": "@polkadot/app-claims", + "version": "0.34.0-beta.123", + "description": "An app for claiming Polkadot tokens", + "main": "index.js", + "scripts": {}, + "author": "Keith Ingram ", + "maintainers": [ + "Keith Ingram ", + "Jaco Greeff " + ], + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@polkadot/react-components": "^0.34.0-beta.123" + } +} diff --git a/packages/app-claims/src/.test-jaco.json b/packages/app-claims/src/.test-jaco.json new file mode 100644 index 0000000000..d8cc3eafe7 --- /dev/null +++ b/packages/app-claims/src/.test-jaco.json @@ -0,0 +1,6 @@ +{ + "address": "0x002309df96687e44280bb72c3818358faeeb699c", + "msg": "Pay KSMs to the Kusama account:88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee", + "sig": "0x55bd020bdbbdc02de34e915effc9b18a99002f4c29f64e22e8dcbb69e722ea6c28e1bb53b9484063fbbfd205e49dcc1f620929f520c9c4c3695150f05a28f52a01", + "version": "2" +} diff --git a/packages/app-claims/src/.test-keith.json b/packages/app-claims/src/.test-keith.json new file mode 100644 index 0000000000..2532820e52 --- /dev/null +++ b/packages/app-claims/src/.test-keith.json @@ -0,0 +1,6 @@ +{ + "address": "0xe71026fcbcecc825f848bcba05cb52bc250bca92", + "msg": "Pay KSMs to the Kusama account:5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", + "sig": "0x98adffe14b1882ba5a861d6aaa10805d52aed56f480e1ece01505a77470f29f15cb4b0a1dc33177761de8270199282baf160f255e1ca0e4c8354b54b0059e40a1c", + "version": "2" +} diff --git a/packages/app-claims/src/Claim.tsx b/packages/app-claims/src/Claim.tsx new file mode 100644 index 0000000000..35093044ba --- /dev/null +++ b/packages/app-claims/src/Claim.tsx @@ -0,0 +1,152 @@ +/* eslint-disable @typescript-eslint/camelcase */ +// Copyright 2017-2019 @polkadot/app-123code 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 { Option } from '@polkadot/types'; +import { BalanceOf, EthereumAddress } from '@polkadot/types/interfaces'; +import { I18nProps } from '@polkadot/react-components/types'; +import { ApiProps } from '@polkadot/react-api/types'; + +import React from 'react'; +import styled from 'styled-components'; +import { withApi, withMulti } from '@polkadot/react-api'; +import { Button, Card } from '@polkadot/react-components'; +import { formatBalance } from '@polkadot/util'; + +import translate from './translate'; +import { addrToChecksum } from './util'; + +const ClaimInner = styled.div` + font-size: 1.15rem; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 12rem; + align-items: center; + margin: 0 1rem; + + h3 { + font-family: monospace; + font-size: 1.5rem; + max-width: 100%; + margin: 0.5rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + h2 { + margin: 0.5rem 0 2rem + font-family: monospace; + font-size: 2.5rem; + font-weight: 200; + } +`; + +interface Props extends ApiProps, I18nProps { + button: React.ReactNode; + ethereumAddress: EthereumAddress | null; +} + +interface State { + claim: BalanceOf | null; + ethereumAddress: EthereumAddress | null; + hasClaim: boolean; + isBusy: boolean; +} + +class Claim extends React.PureComponent { + public static getDerivedStateFromProps ({ ethereumAddress }: Props, state: State): Pick { + if (ethereumAddress) { + return { + ethereumAddress, + hasClaim: state.claim && state.claim.gten(0) + }; + } + + return { + hasClaim: false + }; + } + + public state: State = { + claim: null, + ethereumAddress: null, + hasClaim: false, + isBusy: false + } + + public componentDidMount (): void { + this.fetchClaim(); + } + + public componentDidUpdate (_: Props, prevState: State): void { + const { ethereumAddress } = this.state; + if (ethereumAddress !== prevState.ethereumAddress) { + this.fetchClaim(); + } + } + + public render (): React.ReactNode { + const { button, t } = this.props; + const { claim, ethereumAddress, hasClaim, isBusy } = this.state; + + if (isBusy || !ethereumAddress) { + return null; + } + + return ( + + + {t('Your Ethereum account')} +

{addrToChecksum(ethereumAddress.toString())}

+ {hasClaim && !!claim + ? ( + <> + {t('has a valid claim for')} +

{formatBalance(claim)}

+ + {button} + + + ) + : ( + <> + {t('does not appear to have a valid claim. Please double check that you have signed the transaction correctly on the correct ETH account.')} + + )} +
+
+ ); + } + + private fetchClaim (): void { + const { api } = this.props; + const { ethereumAddress } = this.state; + + if (!ethereumAddress) { + return; + } + + this.setState({ isBusy: true }, (): void => { + api.query.claims + .claims>(ethereumAddress.toHex()) + .then((claim): void => { + this.setState({ + claim: claim.unwrapOr(null), + isBusy: false + }); + }); + }); + } +} + +export default withMulti( + Claim, + translate, + withApi +); diff --git a/packages/app-claims/src/index.tsx b/packages/app-claims/src/index.tsx new file mode 100644 index 0000000000..7986c3a686 --- /dev/null +++ b/packages/app-claims/src/index.tsx @@ -0,0 +1,234 @@ +/* eslint-disable @typescript-eslint/camelcase */ +// Copyright 2017-2019 @polkadot/app-123code 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 { Compact } from '@polkadot/types'; +import { Balance, EcdsaSignature, EthereumAddress } from '@polkadot/types/interfaces'; +import { AppProps, I18nProps } from '@polkadot/react-components/types'; +import { ApiProps } from '@polkadot/react-api/types'; + +import React from 'react'; +import { Trans } from 'react-i18next'; +import styled from 'styled-components'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import { withApi, withMulti } from '@polkadot/react-api'; +import { Button, Card, Columar, Column, Tooltip } from '@polkadot/react-components'; +import { InputNumber } from '@polkadot/react-components/InputNumber'; +import TxModal, { TxModalState, TxModalProps } from '@polkadot/react-components/TxModal'; +import { u8aToHex, u8aToString } from '@polkadot/util'; +import { decodeAddress } from '@polkadot/util-crypto'; + +import ClaimDisplay from './Claim'; +import { recoverFromJSON } from './util'; + +import translate from './translate'; + +enum Step { + Account = 0, + Sign = 1, + Claim = 2, +} + +interface Props extends AppProps, ApiProps, I18nProps, TxModalProps {} + +interface State extends TxModalState { + didCopy: boolean; + ethereumAddress?: EthereumAddress | null; + claim?: Balance | null; + signature?: EcdsaSignature | null; + step: Step; +} + +const Payload = styled.pre` + cursor: copy; + font-family: monospace; + border: 1px dashed #c2c2c2; + background: #fafafa; + padding: 1rem; + width: 100%; + margin: 1rem 0; + white-space: normal; + word-break: break-all; +`; + +const Signature = styled.textarea` + font-family: monospace; + padding: 1rem; + border: 1px solid rgba(34, 36, 38, 0.15); + border-radius: 0.25rem; + margin: 1rem 0; + resize: none; + width: 100%; +`; + +class App extends TxModal { + public constructor (props: Props) { + super(props); + + this.defaultState = { + ...this.defaultState, + claim: null, + didCopy: false, + ethereumAddress: null, + signature: null, + step: 0 + }; + this.state = this.defaultState; + } + + public componentDidUpdate (): void { + if (this.state.didCopy) { + setTimeout((): void => { + this.setState({ didCopy: false }); + }, 1000); + } + } + + public render (): React.ReactNode { + const { api, t } = this.props; + const { accountId, didCopy, ethereumAddress, signature, step } = this.state; + + const payload = accountId + ? ( + u8aToString(Compact.stripLengthPrefix(api.consts.claims.prefix.toU8a(true))) + + u8aToHex(decodeAddress(accountId), -1, false) + ) + : ''; + + return ( +
+
+

+ claim your {InputNumber.units} tokens +

+ + + +

{t('1. Select your Polkadot account')}

+ {this.renderInputAccount()} + {(step === Step.Account) && ( + +
+ ); + } + + protected isDisabled = (): boolean => { + const { accountId, signature } = this.state; + + return !accountId || !signature; + } + + protected isUnsigned = (): boolean => true; + + protected submitLabel = (): React.ReactNode => this.props.t('Redeem'); + + protected txMethod = (): string => 'claims.claim'; + + protected txParams = (): [string | null, EcdsaSignature | null] => { + const { accountId, signature } = this.state; + + return [ + accountId ? accountId.toString() : null, + signature || null + ]; + } + + protected onChangeAccount = (accountId: string | null): void => { + this.setState(({ step }: State): Pick => { + return { + ...( + step > Step.Account + ? this.defaultState + : {} + ), + accountId + }; + }); + } + + protected onChangeSignature = (event: React.SyntheticEvent): void => { + const { value: signatureJson } = event.target as HTMLInputElement; + + this.setState(({ step }: State): Pick => ({ + ...( + step > Step.Sign + ? { step: Step.Sign } + : {} + ), + ...recoverFromJSON(signatureJson) + })); + } + + private onCopy = (): void => { + this.setState({ didCopy: true }); + } + + private setStep = (step: Step): () => void => + (): void => { + this.setState({ step }); + } +} + +export default withMulti( + App, + translate, + withApi +); diff --git a/packages/app-claims/src/secp256k1.d.ts b/packages/app-claims/src/secp256k1.d.ts new file mode 100644 index 0000000000..ec5deada3e --- /dev/null +++ b/packages/app-claims/src/secp256k1.d.ts @@ -0,0 +1,8 @@ +// Copyright 2017-2019 @polkadot/app-123code authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +declare module 'secp256k1/elliptic' { + export function publicKeyConvert (publicKey: Buffer, expanded: boolean): Buffer; + export function recover (msgHash: Buffer, signature: Buffer, recovery: number): Buffer; +} diff --git a/packages/app-claims/src/translate.ts b/packages/app-claims/src/translate.ts new file mode 100644 index 0000000000..8242af08de --- /dev/null +++ b/packages/app-claims/src/translate.ts @@ -0,0 +1,7 @@ +// Copyright 2017-2019 @polkadot/app-123code 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 { withTranslation } from 'react-i18next'; + +export default withTranslation(['sudo', 'ui']); diff --git a/packages/app-claims/src/util.spec.ts b/packages/app-claims/src/util.spec.ts new file mode 100644 index 0000000000..a3d46b8a62 --- /dev/null +++ b/packages/app-claims/src/util.spec.ts @@ -0,0 +1,19 @@ +// Copyright 2017-2019 @polkadot/react-components 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 { hexToU8a } from '@polkadot/util'; + +import { publicToAddr } from './util'; + +describe('publicToAddr', (): void => { + it('converts a publicKey to address', (): void => { + expect( + publicToAddr( + hexToU8a( + '0x836b35a026743e823a90a0ee3b91bf615c6a757e2b60b9e1dc1826fd0dd16106f7bc1e8179f665015f43c6c81f39062fc2086ed849625c06e04697698b21855e' + ) + ) + ).toEqual('0x0BED7ABd61247635c1973eB38474A2516eD1D884'); + }); +}); diff --git a/packages/app-claims/src/util.ts b/packages/app-claims/src/util.ts new file mode 100644 index 0000000000..e5bf7b68ae --- /dev/null +++ b/packages/app-claims/src/util.ts @@ -0,0 +1,111 @@ +/* eslint-disable @typescript-eslint/camelcase */ +// Copyright 2017-2019 @polkadot/app-123code 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 { EthereumAddress, EcdsaSignature } from '@polkadot/types/interfaces'; + +import secp256k1 from 'secp256k1/elliptic'; +import { createType } from '@polkadot/types'; +import { assert, hexToU8a, stringToU8a, u8aToBuffer, u8aConcat } from '@polkadot/util'; +import { keccakAsHex, keccakAsU8a } from '@polkadot/util-crypto'; + +interface RecoveredSignature { + error: Error | null; + ethereumAddress: EthereumAddress | null; + signature: EcdsaSignature | null; +} + +interface SignatureParts { + recovery: number; + signature: Buffer; +} + +// converts an Ethereum address to a checksum representation +export function addrToChecksum (_address: string): string { + const address = _address.toLowerCase(); + const hash = keccakAsHex(address.substr(2)).substr(2); + let result = '0x'; + + for (let n = 0; n < 40; n++) { + result = `${result}${ + parseInt(hash[n], 16) > 7 + ? address[n + 2].toUpperCase() + : address[n + 2] + }`; + } + + return result; +} + +// convert a give public key to an Ethereum address (the last 20 bytes of an _exapnded_ key keccack) +export function publicToAddr (publicKey: Uint8Array): string { + return addrToChecksum(`0x${keccakAsHex(publicKey).slice(-40)}`); +} + +// hash a message for use in signature recovery, adding the standard Ethereum header +export function hashMessage (message: string): Buffer { + const expanded = stringToU8a(`\x19Ethereum Signed Message:\n${message.length.toString()}${message}`); + const hashed = keccakAsU8a(expanded); + + return u8aToBuffer(hashed); +} + +// split is 65-byte signature into the r, s (combined) and recovery number (derived from v) +export function sigToParts (_signature: string): SignatureParts { + const signature = hexToU8a(_signature); + + assert(signature.length === 65, `Invalid signature length, expected 65 found ${signature.length}`); + + let v = signature[64]; + + if (v < 27) { + v += 27; + } + + const recovery = v - 27; + + assert(recovery === 0 || recovery === 1, `Invalid signature v value`); + + return { + recovery, + signature: u8aToBuffer(signature.slice(0, 64)) + }; +} + +// recover an address from a given message and a recover/signature combination +export function recoverAddress (message: string, { recovery, signature }: SignatureParts): string { + const msgHash = hashMessage(message); + const senderPubKey = secp256k1.recover(msgHash, signature, recovery); + + return publicToAddr( + secp256k1.publicKeyConvert(senderPubKey, false).slice(1) + ); +} + +// recover an address from a signature JSON (as supplied by e.g. MyCrypto) +export function recoverFromJSON (signatureJson: string | null): RecoveredSignature { + try { + const { msg, sig } = JSON.parse(signatureJson || '{}'); + + if (!msg || !sig) { + throw new Error('Invalid signature object'); + } + + const parts = sigToParts(sig); + + return { + error: null, + ethereumAddress: createType('EthereumAddress', recoverAddress(msg, parts)), + signature: createType('EcdsaSignature', u8aConcat(parts.signature, new Uint8Array([parts.recovery]))) + }; + } catch (error) { + console.error(error); + + return { + error, + ethereumAddress: null, + signature: null + }; + } +} diff --git a/packages/apps-routing/src/claims.ts b/packages/apps-routing/src/claims.ts new file mode 100644 index 0000000000..583c1d3efc --- /dev/null +++ b/packages/apps-routing/src/claims.ts @@ -0,0 +1,23 @@ +// Copyright 2017-2019 @polkadot/apps-routing 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 { Routes } from './types'; + +import Claims from '@polkadot/app-claims'; + +export default ([ + { + Component: Claims, + display: { + needsApi: [ + 'query.claims.claims' + ] + }, + i18n: { + defaultValue: 'Claim Tokens' + }, + icon: 'star', + name: 'claims' + } +] as Routes); diff --git a/packages/apps-routing/src/index.ts b/packages/apps-routing/src/index.ts index 0d6a4279c0..1918a851fa 100644 --- a/packages/apps-routing/src/index.ts +++ b/packages/apps-routing/src/index.ts @@ -9,9 +9,10 @@ import appSettings from '@polkadot/ui-settings'; import template from './123code'; import accounts from './accounts'; import addressbook from './addressbook'; +import claims from './claims'; import contracts from './contracts'; import council from './council'; -import dashboard from './dashboard'; +// import dashboard from './dashboard'; import democracy from './democracy'; import explorer from './explorer'; import extrinsics from './extrinsics'; @@ -27,10 +28,11 @@ import treasury from './treasury'; const routes: Routes = appSettings.uiMode === 'light' ? ([] as Routes).concat( - dashboard, + // dashboard, explorer, accounts, addressbook, + claims, transfer, null, staking, @@ -38,14 +40,14 @@ const routes: Routes = appSettings.uiMode === 'light' council, // TODO Not sure about the inclusion of treasury & parachains here null, - settings, - template + settings ) : ([] as Routes).concat( - dashboard, + // dashboard, explorer, accounts, addressbook, + claims, transfer, null, staking, diff --git a/packages/react-components/src/Card.tsx b/packages/react-components/src/Card.tsx index 46c8c83221..9663a8758c 100644 --- a/packages/react-components/src/Card.tsx +++ b/packages/react-components/src/Card.tsx @@ -5,17 +5,22 @@ import React from 'react'; import styled from 'styled-components'; +import { classes } from './util'; + interface Props { children: React.ReactNode; className?: string; + isError?: boolean; + isSuccess?: boolean; + withBottomMargin?: boolean; } class Card extends React.PureComponent { public render (): React.ReactNode { - const { children, className } = this.props; + const { children, className, isError, isSuccess, withBottomMargin } = this.props; return ( -
+
{children}
); @@ -28,6 +33,27 @@ export default styled(Card)` min-width: 24%; justify-content: space-around; + &.error { + background: rgba(255, 0, 0, 0.05); + + &, h1, h2, h3, h4, h5, h6, p { + color: rgba(156, 0, 0) !important; + } + } + + &.success { + border: 1px solid rgb(168, 255, 136); + background: rgba(0, 255, 0, 0.05); + + &, h1, h2, h3, h4, h5, h6, p { + color: rgba(34, 125, 0) !important; + } + } + + &.withBottomMargin { + margin-bottom: 1.5rem; + } + i.help.circle.icon, .ui.button.mini, .ui.button.tiny, diff --git a/packages/react-components/src/Collection.tsx b/packages/react-components/src/Collection.tsx index 1119df1549..dc854408dd 100644 --- a/packages/react-components/src/Collection.tsx +++ b/packages/react-components/src/Collection.tsx @@ -14,6 +14,7 @@ export interface CollectionProps extends I18nProps { headerText?: React.ReactNode; isEmpty?: boolean; emptyText?: React.ReactNode; + showEmptyText?: boolean; } export interface CollectionState { @@ -78,6 +79,10 @@ export default class Collection

{headerText}

@@ -91,12 +96,16 @@ export default class Collection

- {emptyText || t('No items')} + {emptyText}
); diff --git a/packages/react-components/src/Inset.tsx b/packages/react-components/src/Inset.tsx index 0b1a06a2cb..ac287e1214 100644 --- a/packages/react-components/src/Inset.tsx +++ b/packages/react-components/src/Inset.tsx @@ -14,6 +14,8 @@ export interface InsetProps extends RouteComponentProps { className?: string; children?: React.ReactNode; isCollapsible?: boolean; + isError?: boolean; + isSuccess?: boolean; header?: React.ReactNode; href?: string | null; withTopMargin?: boolean; @@ -30,7 +32,7 @@ class Inset extends React.PureComponent { }; public render (): React.ReactNode { - const { children, className, isCollapsible, header, href, withBottomMargin, withTopMargin } = this.props; + const { children, className, isCollapsible, isError, isSuccess, header, href, withBottomMargin, withTopMargin } = this.props; const { isCollapsed } = this.state; if (!children) { @@ -44,6 +46,8 @@ class Inset extends React.PureComponent { 'ui--Inset', href && 'as-link', isCollapsible && 'collapsible', + (isError && !isSuccess) && 'error', + (!isError && isSuccess) && 'success', withBottomMargin && 'bottom-margin', withTopMargin && 'top-margin', className @@ -106,6 +110,23 @@ export default withRouter( margin-top: 2rem; } + &.error { + background: rgba(255, 0, 0, 0.05); + + &, h1, h2, h3, h4, h5, h6, p { + color: rgba(156, 0, 0) !important; + } + } + + &.success { + border: 1px solid rgb(168, 255, 136); + background: rgba(0, 255, 0, 0.05); + + &, h1, h2, h3, h4, h5, h6, p { + color: rgba(34, 125, 0) !important; + } + } + .header { cursor: pointer; height: 2rem; diff --git a/packages/react-components/src/TxModal.tsx b/packages/react-components/src/TxModal.tsx index 6e12ee3e23..2a268bef7f 100644 --- a/packages/react-components/src/TxModal.tsx +++ b/packages/react-components/src/TxModal.tsx @@ -106,6 +106,8 @@ class TxModal

extends TxComponen }); } + protected isUnsigned: () => boolean = (): boolean => false; + protected txMethod: () => string = (): string => ''; protected txParams: () => any[] = (): any[] => []; @@ -147,7 +149,11 @@ class TxModal

extends TxComponen return ( { const { t } = this.props; const { currentItem, isSendable, isV2 } = this.state; - if (!isV2 || !isSendable || !currentItem) { + if (!isV2 || !isSendable || !currentItem || currentItem.isUnsigned) { return null; } diff --git a/tsconfig.json b/tsconfig.json index 2da829b786..25bcd541f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,8 @@ "@polkadot/app-accounts/*": [ "packages/app-accounts/src/*" ], "@polkadot/app-address-book": [ "packages/app-address-book/src" ], "@polkadot/app-address-book/*": [ "packages/app-address-book/src/*" ], + "@polkadot/app-claims": [ "packages/app-claims/src" ], + "@polkadot/app-claims/*": [ "packages/app-claims/src/*" ], "@polkadot/app-contracts": [ "packages/app-contracts/src" ], "@polkadot/app-contracts/*": [ "packages/app-contracts/src/*" ], "@polkadot/app-council": [ "packages/app-council/src" ],