Add attestation to claims (#2734)

* init

* Attest component

* Add top notification bar

* better english

* back to 3 steps

* clean up

* in the middle of the refactoring

* Make TS pass

* Clea up some comments

* isPreclaimed

* isOldClaimProcess

* Attest vs Claim (#2737)

* Choose between Claim and Attest

* fix lint

* fix flow between claim and attest (#2738)

* Jaco's comments + Fetch StatementKind (#2741)

* Fix small bug in flow

* Add condition for the warning banner (#2742)

* prevent crash for older chains

* with useEffect

* should work

* back to something simple

* remove hoc

* fix CI

* Add ETH address field for non preclaimed addresses (#2746)

* Eth field

* fix

* remove stray logs

* Update packages/page-claims/src/index.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* Update packages/page-claims/src/index.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* Update packages/apps/src/overlays/Attest.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* Show the amount of token that will be claimed (#2747)

* show amount in attest

* fix isApiReady

* fix lint

* Use counter instead of overlay (#2750)

* Use counter instead of overlay

* Add warning to claim (#2752)

* add warning

* css cleanup

* fix unused const

Co-authored-by: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com>

* Update packages/page-claims/src/Warning.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* Update packages/page-claims/src/index.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* Update packages/page-claims/src/index.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* Update packages/page-claims/src/index.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* Update packages/page-claims/src/index.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* address comment shortcut

* set Ethereum address outside transfor and fix typos

* lint

* extract claims.preclaims to a hook

* address comments EthereumAddress type and avoid casting

* Missing isUnsigned

* use AddressMini in warning

* use a proper step for eth address field

* Add loading state

* remove log

* unwrapOrNull

* Update packages/page-claims/src/Warning.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* fix for account plural translation

* lint

* Add getStatement function

* nits

* Fix lint

* Update packages/page-claims/src/Warning.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* add statement

* statement component

* address comment unWrapOr

* test styling with long md

* add md to jest

* Update packages/page-claims/src/Statement.tsx

Co-authored-by: Amaury Martiny <amaury.martiny@protonmail.com>

* copy text before

* lint

* Remove EthreumAddress type to avoid unexpected behaviors (#2796)

* remove EthreumAddress type

* createType when needed

* revert

* typo

* Enhance UX for claim process (#2797)

* update content

* init

* comment

* Add HTML components for statements (#2798)

* Add HTML components for statements

* Update packages/page-claims/src/statements/saft.tsx

* Update to final statements

* More nits

* Nits

* OnSuccess

* prevent display of empty statement when attested

* Small fixmes

* Update packages/page-claims/src/index.tsx

* fix isError not being red as expected

* remove word-break

* statement text in black

* attestation for -> accounc balance

* Update packages/page-claims/src/Statement.tsx

Co-authored-by: Logan Saether <x@logansaether.com>

* margin left instead of space

* I agree

* Use iframe

* update saft to QmXEkMahfhHJPzT3RjkXiZVFi77ZeVeuxtAjhojGRNYckz

* remove hash and duplications (#2814)

Co-authored-by: Amaury Martiny <amaury.martiny@protonmail.com>
Co-authored-by: Jaco Greeff <jacogr@gmail.com>
Co-authored-by: Logan Saether <x@logansaether.com>
This commit is contained in:
Thibaut Sardan 2020-05-26 11:49:12 +02:00 committed by GitHub
parent 8e00005cd5
commit 0091fc6e57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 627 additions and 234 deletions

View File

@ -4,7 +4,7 @@
import { Route } from './types';
import Claims from '@polkadot/app-claims';
import Claims, { useCounter } from '@polkadot/app-claims';
export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
return {
@ -17,6 +17,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
},
icon: 'star',
name: 'claims',
text: t<string>('nav.claims', 'Claim Tokens', { ns: 'apps-routing' })
text: t<string>('nav.claims', 'Claim Tokens', { ns: 'apps-routing' }),
useCounter
};
}

View File

@ -0,0 +1,87 @@
// Copyright 2017-2020 @polkadot/app-claims 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 React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Button, Card, TxButton } from '@polkadot/react-components';
import { TxCallback } from '@polkadot/react-components/Status/types';
import { useApi } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import { Option } from '@polkadot/types';
import { BalanceOf, EthereumAddress, StatementKind } from '@polkadot/types/interfaces';
import { ClaimStyles } from './Claim';
import Statement from './Statement';
import { useTranslation } from './translate';
import { getStatement } from './util';
interface Props {
accountId: string;
className?: string;
ethereumAddress: EthereumAddress | null;
onSuccess?: TxCallback;
statementKind?: StatementKind;
systemChain: string;
}
function Attest ({ accountId, className, ethereumAddress, onSuccess, statementKind, systemChain }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const [claimValue, setClaimValue] = useState<BalanceOf | null>(null);
const [isBusy, setIsBusy] = useState(false);
useEffect((): void => {
if (!ethereumAddress) {
return;
}
setIsBusy(true);
api.query.claims
.claims<Option<BalanceOf>>(ethereumAddress)
.then((claim): void => {
setClaimValue(claim.unwrapOr(null));
setIsBusy(false);
})
.catch((): void => setIsBusy(false));
}, [api, ethereumAddress]);
if (isBusy) {
return null;
}
const hasClaim = claimValue && claimValue.gten(0);
const statementSentence = getStatement(systemChain, statementKind)?.sentence;
if (!hasClaim || !statementSentence) {
return null;
}
return (
<Card isSuccess>
<div className={className}>
<Statement
kind={statementKind}
systemChain={systemChain}
/>
<h3><FormatBalance label={t<string>('Account balance:')}
value={claimValue} /></h3>
<Button.Group>
<TxButton
accountId={accountId}
icon='send'
isDisabled={!statementSentence}
isPrimary
label={t<string>('I agree')}
onSuccess={onSuccess}
params={[statementSentence]}
tx='claims.attest'
/>
</Button.Group>
</div>
</Card>
);
}
export default React.memo(styled(Attest)`${ClaimStyles}`);

View File

@ -3,52 +3,75 @@
// 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 { BalanceOf, EthereumAddress, StatementKind } from '@polkadot/types/interfaces';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Button, Card } from '@polkadot/react-components';
import { Button, Card, TxButton } from '@polkadot/react-components';
import { TxCallback } from '@polkadot/react-components/Status/types';
import { useApi } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import { useTranslation } from './translate';
import { addrToChecksum } from './util';
import { addrToChecksum, getStatement } from './util';
interface Props {
button: React.ReactNode;
accountId: string;
className?: string;
ethereumAddress: EthereumAddress | null;
ethereumSignature: string | null;
// Do we sign with `claims.claimAttest` (new) instead of `claims.claim` (old)?
isOldClaimProcess: boolean;
onSuccess?: TxCallback;
statementKind?: StatementKind;
systemChain: string;
}
function Claim ({ button, className = '', ethereumAddress }: Props): React.ReactElement<Props> | null {
interface ConstructTx {
params?: any[];
tx?: string;
}
// Depending on isOldClaimProcess, construct the correct tx.
function constructTx (
systemChain: string,
accountId: string,
ethereumSignature: string | null,
kind: StatementKind | undefined,
isOldClaimProcess: boolean
): ConstructTx {
if (!ethereumSignature || !kind) {
return {};
}
return isOldClaimProcess
? { params: [accountId, ethereumSignature], tx: 'claims.claim' }
: { params: [accountId, ethereumSignature, getStatement(systemChain, kind)?.sentence], tx: 'claims.claimAttest' };
}
function Claim ({ accountId, className = '', ethereumAddress, ethereumSignature, isOldClaimProcess, onSuccess, statementKind, systemChain }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const [claimValue, setClaimValue] = useState<BalanceOf | null>(null);
const [claimAddress, setClaimAddress] = useState<EthereumAddress | null>(null);
const [isBusy, setIsBusy] = useState(false);
const _fetchClaim = useCallback(
(address: EthereumAddress): void => {
setIsBusy(true);
api.query.claims
.claims<Option<BalanceOf>>(address)
.then((claim): void => {
setClaimValue(claim.unwrapOr(null));
setIsBusy(false);
})
.catch((): void => setIsBusy(false));
},
[api]
);
useEffect((): void => {
setClaimAddress(ethereumAddress);
if (!ethereumAddress) {
return;
}
ethereumAddress && _fetchClaim(ethereumAddress);
}, [_fetchClaim, ethereumAddress]);
setIsBusy(true);
if (isBusy || !claimAddress) {
api.query.claims
.claims<Option<BalanceOf>>(ethereumAddress)
.then((claim): void => {
setClaimValue(claim.unwrapOr(null));
setIsBusy(false);
})
.catch((): void => setIsBusy(false));
}, [api, ethereumAddress]);
if (!ethereumAddress || isBusy) {
return null;
}
@ -61,13 +84,23 @@ function Claim ({ button, className = '', ethereumAddress }: Props): React.React
>
<div className={className}>
{t<string>('Your Ethereum account')}
<h3>{addrToChecksum(claimAddress.toString())}</h3>
<h3>{addrToChecksum(ethereumAddress.toString())}</h3>
{hasClaim && claimValue
? (
<>
{t<string>('has a valid claim for')}
<h2><FormatBalance value={claimValue} /></h2>
<Button.Group>{button}</Button.Group>
<Button.Group>
<TxButton
accountId={accountId}
icon='send'
isPrimary
isUnsigned
label={t('Claim')}
onSuccess={onSuccess}
{...constructTx(systemChain, accountId, ethereumSignature, statementKind, isOldClaimProcess)}
/>
</Button.Group>
</>
)
: (
@ -80,29 +113,31 @@ function Claim ({ button, className = '', ethereumAddress }: Props): React.React
);
}
export default React.memo(styled(Claim)`
font-size: 1.15rem;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 12rem;
align-items: center;
margin: 0 1rem;
export const ClaimStyles = `
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;
}
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;
}
`);
h2 {
margin: 0.5rem 0 2rem;
font-family: monospace;
font-size: 2.5rem;
font-weight: 200;
}
`;
export default React.memo(styled(Claim)`${ClaimStyles}`);

View File

@ -0,0 +1,82 @@
// Copyright 2017-2020 @polkadot/app-claims 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 { StatementKind } from '@polkadot/types/interfaces';
import React from 'react';
import styled from 'styled-components';
import { useTranslation } from './translate';
import { getStatement } from './util';
export interface Props {
className?: string;
kind?: StatementKind;
systemChain: string;
}
// Get the full hardcoded text for a statement
function StatementFullText ({ statementUrl, systemChain }: { statementUrl?: string; systemChain: string }): React.ReactElement | null {
const { t } = useTranslation();
switch (systemChain) {
case 'Polkadot CC1': {
if (!statementUrl) {
return null;
}
return <iframe src={statementUrl} />;
}
default:
return <p>{t('Warning: we did not find any attest statement for {{chain}}', { replace: { chain: systemChain } })}</p>;
}
}
function Statement ({ className, kind, systemChain }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const statementUrl = getStatement(systemChain, kind)?.url;
return (
<div className={className}>
{t('Please read these terms and conditions carefully. By submitting this statement, you are deemed to have accepted these Terms and Conditions. If you do not agree to these terms, please refrain from accessing or proceeding. You can also find them at:')}
<a className='statementUrl'
href={statementUrl}
rel='noopener noreferrer'
target='_blank'>{statementUrl}</a>
<div className='statement'>
<StatementFullText
statementUrl={statementUrl}
systemChain={systemChain}
/>
</div>
</div>
);
}
export default React.memo(styled(Statement)`
.statement{
border: 1px solid #c2c2c2;
background: #f2f2f2;
height: 15rem;
padding: 1rem;
width: 100%;
margin: 1rem 0;
white-space: normal;
p {
color: #4e4e4e !important;
}
iframe {
border: 0;
height: 100%;
width: 100%;
}
}
.statementUrl{
margin-left: 0.3rem
}
`);

View File

@ -0,0 +1,57 @@
// Copyright 2017-2020 @polkadot/app-claims 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 { AddressMini, Card } from '@polkadot/react-components';
import React from 'react';
import styled from 'styled-components';
import { useTranslation } from './translate';
import usePreclaimPolkadotAddresses from './usePreclaimPolkadotAddresses';
export interface Props{
className?: string;
}
function Warning ({ className }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const needsAttestArray = usePreclaimPolkadotAddresses();
if (!needsAttestArray || !needsAttestArray.length) {
return null;
}
return (
<Card isError>
<div className={className}>
{
needsAttestArray.length > 1
? t('You need to sign an attestation for the following accounts:')
: t('You need to sign an attestation for the following account:')
}{
needsAttestArray.map((address) => (
<AddressMini
key={address}
value={address}
/>
))
}
</div>
</Card>
);
}
export default React.memo(styled(Warning)`
font-size: 1.15rem;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 8rem;
align-items: center;
margin: 0 1rem;
.ui--AddressMini-address {
max-width: 20rem;
}
`);

View File

@ -2,41 +2,36 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Balance, EcdsaSignature, EthereumAddress } from '@polkadot/types/interfaces';
import { AppProps, I18nProps } from '@polkadot/react-components/types';
import { ApiProps } from '@polkadot/react-api/types';
import { Option } from '@polkadot/types';
import { EcdsaSignature, EthereumAddress, StatementKind } from '@polkadot/types/interfaces';
import React from 'react';
import React, { useState, useCallback, useEffect } 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/hoc';
import { Button, Card, Columar, Column, InputAddress, Tooltip } from '@polkadot/react-components';
import { Button, Card, Columar, Column, Input, InputAddress, Tooltip } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { TokenUnit } 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 { useTranslation } from './translate';
import { recoverFromJSON, getStatement } from './util';
import AttestDisplay from './Attest';
import ClaimDisplay from './Claim';
import { recoverFromJSON } from './util';
import Statement from './Statement';
import Warning from './Warning';
import translate from './translate';
export { default as useCounter } from './useCounter';
enum Step {
Account = 0,
Sign = 1,
Claim = 2,
ETHAddress = 1,
Sign = 2,
Claim = 3,
}
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 PRECLAIMS_LOADING = 'PRECLAIMS_LOADING';
// FIXME no embedded components (hossible to tweak)
const Payload = styled.pre`
@ -73,183 +68,250 @@ const Signature = styled.textarea`
}
`;
// FIXME Not React.FC yet
class ClaimsApp extends TxModal<Props, State> {
constructor (props: Props) {
super(props);
function ClaimsApp (): React.ReactElement {
const [didCopy, setDidCopy] = useState(false);
const [ethereumAddress, setEthereumAddress] = useState<string | undefined | null>(null);
const [signature, setSignature] = useState<EcdsaSignature | null>(null);
const [step, setStep] = useState<Step>(Step.Account);
const [accountId, setAccountId] = useState<string | null>(null);
const { api, systemChain } = useApi();
const { t } = useTranslation();
this.defaultState = {
...this.defaultState,
claim: null,
didCopy: false,
ethereumAddress: null,
signature: null,
step: 0
};
this.state = this.defaultState;
}
// This preclaimEthereumAddress holds the result of `api.query.claims.preclaims`:
// - an `EthereumAddress` when there's a preclaim
// - null if no preclaim
// - `PRECLAIMS_LOADING` if we're fetching the results
const [preclaimEthereumAddress, setPreclaimEthereumAddress] = useState<string | null | undefined | typeof PRECLAIMS_LOADING>(PRECLAIMS_LOADING);
const isPreclaimed = !!preclaimEthereumAddress && preclaimEthereumAddress !== PRECLAIMS_LOADING;
public componentDidUpdate (): void {
if (this.state.didCopy) {
// Everytime we change account, reset everything, and check if the accountId
// has a preclaim.
useEffect(() => {
if (!accountId) {
return;
}
setStep(Step.Account);
setEthereumAddress(null);
setPreclaimEthereumAddress(PRECLAIMS_LOADING);
if (!api.query.claims || !api.query.claims.preclaims) {
return setPreclaimEthereumAddress(null);
}
api.query.claims
.preclaims<Option<EthereumAddress>>(accountId)
.then((preclaim): void => {
const address = preclaim.unwrapOr(null)?.toString();
setEthereumAddress(address);
setPreclaimEthereumAddress(address);
})
.catch((): void => setPreclaimEthereumAddress(null));
}, [accountId, api.query.claims, api.query.claims.preclaims]);
// Old claim process used `api.tx.claims.claim`, and didn't have attest
const isOldClaimProcess = !api.tx.claims.claimAttest;
useEffect(() => {
if (didCopy) {
setTimeout((): void => {
this.setState({ didCopy: false });
setDidCopy(false);
}, 1000);
}
}
}, [didCopy]);
public render (): React.ReactNode {
const { api, systemChain = '', t } = this.props;
const { accountId, didCopy, ethereumAddress, signature, step } = this.state;
const goToStepAccount = useCallback(() => {
setStep(Step.Account);
}, []);
const prefix = u8aToString(api.consts.claims.prefix.toU8a(true));
const payload = accountId
? `${prefix}${u8aToHex(decodeAddress(accountId), -1, false)}`
: '';
const goToStepSign = useCallback(() => {
setStep(Step.Sign);
}, []);
return (
<main>
<header />
<h1>
<Trans>claim your <em>{TokenUnit.abbr}</em> tokens</Trans>
</h1>
<Columar>
<Column>
<Card withBottomMargin>
<h3>{t<string>('1. Select your {{chain}} account', {
replace: {
chain: systemChain
}
})}</h3>
<InputAddress
defaultValue={this.state.accountId}
help={t<string>('The account you want to claim to.')}
label={t<string>('claim to account')}
onChange={this.onChangeAccount}
type='all'
/>
{(step === Step.Account) && (
<Button.Group>
<Button
icon='sign-in'
label={t<string>('Continue')}
onClick={this.setStep(Step.Sign)}
/>
</Button.Group>
)}
</Card>
{(step >= Step.Sign && !!accountId) && (
<Card>
<h3>{t<string>('2. Sign ETH transaction')}</h3>
<CopyToClipboard
onCopy={this.onCopy}
text={payload}
>
<Payload
data-for='tx-payload'
data-tip
>
{payload}
</Payload>
</CopyToClipboard>
<Tooltip
place='right'
text={didCopy ? t<string>('copied') : t<string>('click to copy')}
trigger='tx-payload'
const goToStepClaim = useCallback(() => {
setStep(Step.Claim);
}, []);
// Depending on the account, decide which step to show.
const handleAccountStep = useCallback(() => {
if (isPreclaimed) {
goToStepClaim();
} else if (ethereumAddress || isOldClaimProcess) {
goToStepSign();
} else {
setStep(Step.ETHAddress);
}
}, [ethereumAddress, goToStepClaim, goToStepSign, isPreclaimed, isOldClaimProcess]);
const onChangeSignature = useCallback((event: React.SyntheticEvent<Element>) => {
const { value: signatureJson } = event.target as HTMLInputElement;
const { ethereumAddress, signature } = recoverFromJSON(signatureJson);
setEthereumAddress(ethereumAddress?.toString());
setSignature(signature);
}, []);
const onChangeEthereumAddress = useCallback((value: string) => {
// FIXME We surely need a better check than just a trim
setEthereumAddress(value.trim());
}, []);
const onCopy = useCallback(() => {
setDidCopy(true);
}, []);
// If it's 1/ not preclaimed and 2/ not the old claiming process, fetch the
// statement kind to sign.
const statementKind = useCall<StatementKind | null>(!isPreclaimed && !isOldClaimProcess && !!ethereumAddress && api.query.claims.signing, [ethereumAddress], {
transform: (option: Option<StatementKind>) => option.unwrapOr(null)
});
const statementSentence = getStatement(systemChain, statementKind)?.sentence || '';
const prefix = u8aToString(api.consts.claims.prefix.toU8a(true));
const payload = accountId
? `${prefix}${u8aToHex(decodeAddress(accountId), -1, false)}${statementSentence}`
: '';
return (
<main>
<header />
{!isOldClaimProcess && <Warning />}
<h1>
<Trans>claim your <em>{TokenUnit.abbr}</em> tokens</Trans>
</h1>
<Columar>
<Column>
<Card withBottomMargin>
<h3>{t<string>('1. Select your {{chain}} account', {
replace: {
chain: systemChain
}
})}</h3>
<InputAddress
defaultValue={accountId}
help={t<string>('The account you want to claim to.')}
label={t<string>('claim to account')}
onChange={setAccountId}
type='all'
/>
{(step === Step.Account) && (
<Button.Group>
<Button
icon='sign-in'
isDisabled={preclaimEthereumAddress === PRECLAIMS_LOADING}
label={preclaimEthereumAddress === PRECLAIMS_LOADING
? t<string>('Loading')
: t<string>('Continue')
}
onClick={handleAccountStep}
/>
<div>
{t<string>('Copy the above string and sign an Ethereum transaction with the account you used during the pre-sale in the wallet of your choice, using the string as the payload, and then paste the transaction signature object below')}
:
</div>
<Signature
onChange={this.onChangeSignature}
placeholder={`{\n "address": "0x ...",\n "msg": "${prefix}:...",\n "sig": "0x ...",\n "version": "2"\n}`}
rows={10}
</Button.Group>
)}
</Card>
{
// We need to know the ethereuem address only for the new process
// to be able to know the statement kind so that the users can sign it
(step >= Step.ETHAddress && !isPreclaimed && !isOldClaimProcess) && (
<Card withBottomMargin>
<h3>{t<string>('2. Enter the ETH address from the sale.')}</h3>
<Input
autoFocus
className='full'
help={t<string>('The the Ethereum address you used during the pre-sale (starting by "0x")')}
label={t<string>('Pre-sale ethereum address')}
onChange={onChangeEthereumAddress}
value={ethereumAddress || ''}
/>
{(step === Step.Sign) && (
{(step === Step.ETHAddress) && (
<Button.Group>
<Button
icon='sign-in'
isDisabled={!accountId || !signature}
label={t<string>('Confirm claim')}
onClick={this.setStep(Step.Claim)}
isDisabled={!ethereumAddress}
label={t<string>('Continue')}
onClick={goToStepSign}
/>
</Button.Group>
)}
</Card>
)}
</Column>
<Column showEmptyText={false}>
{(step >= Step.Claim) && (
<ClaimDisplay
button={this.renderTxButton()}
{(step >= Step.Sign && !isPreclaimed) && (
<Card>
{(isOldClaimProcess || !!statementKind)
? (
<>
<h3>{t<string>('{{step}}. Sign with your ETH address',
{ replace: { step: isOldClaimProcess ? '2' : '3' } })}</h3>
{!isOldClaimProcess && (<Statement
kind={statementKind}
systemChain={systemChain}
/>)}
<div>{t<string>('Copy the following string and sign it with the Ethreum account you used during the pre-sale in the wallet of your choice, using the string as the payload, and then paste the transaction signature object below:')}</div>
<CopyToClipboard
onCopy={onCopy}
text={payload}
>
<Payload
data-for='tx-payload'
data-tip
>
{payload}
</Payload>
</CopyToClipboard>
<Tooltip
place='right'
text={didCopy ? t<string>('copied') : t<string>('click to copy')}
trigger='tx-payload'
/>
<Signature
onChange={onChangeSignature}
placeholder={`{\n "address": "0x ...",\n "msg": "${prefix}:...",\n "sig": "0x ...",\n "version": "2"\n}`}
rows={10}
/>
{(step === Step.Sign) && (
<Button.Group>
<Button
icon='sign-in'
isDisabled={!accountId || !signature}
label={t<string>('Confirm claim')}
onClick={goToStepClaim}
/>
</Button.Group>
)}
</>
)
: <div>{t<string>('This Ethereum address does not have a claim.')}</div>
}
</Card>
)}
</Column>
<Column showEmptyText={false}>
{(step >= Step.Claim) && (
isPreclaimed
? <AttestDisplay
accountId={accountId}
ethereumAddress={ethereumAddress}
onSuccess={goToStepAccount}
statementKind={statementKind}
systemChain={systemChain}
/>
)}
</Column>
</Columar>
</main>
);
}
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<State, never> => {
return {
...(
step > Step.Account
? this.defaultState
: {}
),
accountId
};
});
}
protected onChangeSignature = (event: React.SyntheticEvent<Element>): void => {
const { value: signatureJson } = event.target as HTMLInputElement;
this.setState(({ step }: State): Pick<State, never> => ({
...(
step > Step.Sign
? { step: Step.Sign }
: {}
),
...recoverFromJSON(signatureJson)
}));
}
private onCopy = (): void => {
this.setState({ didCopy: true });
}
private setStep = (step: Step): () => void =>
(): void => {
this.setState({ step });
}
: <ClaimDisplay
accountId={accountId}
ethereumAddress={ethereumAddress}
ethereumSignature={signature}
isOldClaimProcess={isOldClaimProcess}
onSuccess={goToStepAccount}
statementKind={statementKind}
systemChain={systemChain}
/>
)}
</Column>
</Columar>
</main>
);
}
export default withMulti(
ClaimsApp,
translate,
withApi
);
export default React.memo(ClaimsApp);

View File

@ -0,0 +1,11 @@
// Copyright 2017-2020 @polkadot/app-settings 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 usePreclaimPolkadotAddresses from './usePreclaimPolkadotAddresses';
export default function useCounter (): number {
const needAttest = usePreclaimPolkadotAddresses();
return needAttest ? needAttest.length : 0;
}

View File

@ -0,0 +1,27 @@
// Copyright 2017-2020 @polkadot/app-settings 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 { EthereumAddress } from '@polkadot/types/interfaces';
import { useAccounts, useApi, useCall } from '@polkadot/react-hooks';
export default function usePreclaimPolkadotAddresses (): string[] {
const { allAccounts } = useAccounts();
const { api } = useApi();
// Find accounts that need attest. They are accounts that
// - already preclaimed,
// - didn't sign the attest yet.
// `claims.preclaims` returns the ethereum address for these accounts.
const needAttest = useCall<string[]>(api.query.claims?.preclaims?.multi, [allAccounts], {
transform: (preclaims: Option<EthereumAddress>[]) =>
preclaims
.map((opt, index): [boolean, string] => [opt.isSome, allAccounts[index]])
.filter(([isSome]) => isSome)
.map(([, address]) => address)
});
return needAttest || [];
}

View File

@ -2,7 +2,7 @@
// 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 { EthereumAddress, EcdsaSignature, StatementKind } from '@polkadot/types/interfaces';
import secp256k1 from 'secp256k1/elliptic';
import { registry } from '@polkadot/react-api';
@ -108,3 +108,34 @@ export function recoverFromJSON (signatureJson: string | null): RecoveredSignatu
};
}
}
export interface Statement {
sentence: string;
url: string;
}
export function getStatement (network: string, kind?: StatementKind | null): Statement | undefined {
switch (network) {
case 'Polkadot CC1': {
if (!kind) {
return undefined;
}
const url = kind.isRegular
? 'https://statement.polkadot.network/regular.html'
: 'https://statement.polkadot.network/saft.html';
const hash = kind.isRegular
? 'Qmc1XYqT6S39WNp2UeiRUrZichUWUPpGEThDE6dAb3f6Ny'
: 'QmXEkMahfhHJPzT3RjkXiZVFi77ZeVeuxtAjhojGRNYckz';
return {
sentence: `I hereby agree to the terms of the statement whose SHA-256 multihash is ${hash}. (This may be found at the URL: ${url})`,
url
};
}
default:
return undefined;
}
}

View File

@ -15,7 +15,7 @@ interface Props {
function Card ({ children, className = '', isError, isSuccess, withBottomMargin }: Props): React.ReactElement<Props> {
return (
<article className={`ui--Card ${className} ${(isError && !isSuccess) ? 'error' : ''}, ${(!isError && isSuccess) ? 'success' : ''} ${withBottomMargin ? 'withBottomMargin' : ''}`}>
<article className={`ui--Card ${className} ${(isError && !isSuccess) ? 'error' : ''} ${(!isError && isSuccess) ? 'success' : ''} ${withBottomMargin ? 'withBottomMargin' : ''}`}>
{children}
</article>
);