import { ContractWrappers } from '@0x/contract-wrappers'; import { signatureUtils } from '@0x/order-utils'; import { LedgerSubprovider } from '@0x/subproviders'; import { ECSignature, SignatureType } from '@0x/types'; import { BigNumber, signTypedDataUtils } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import '@reach/dialog/styles.css'; import { ZeroExProvider } from 'ethereum-types'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; import * as React from 'react'; import styled from 'styled-components'; import { Button } from 'ts/components/button'; import { Input } from 'ts/components/modals/input'; import { Heading, Paragraph } from 'ts/components/text'; import { LedgerSignNote } from 'ts/pages/governance/ledger_sign_note'; import { PreferenceSelecter } from 'ts/pages/governance/preference_selecter'; import { colors } from 'ts/style/colors'; import { InjectedProvider } from 'ts/types'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; import { utils } from 'ts/utils/utils'; export enum VoteValue { Yes = 'Yes', No = 'No', } export interface VoteInfo { userBalance: BigNumber; voteValue: VoteValue; } interface Props { onDismiss?: () => void; onWalletConnected?: (providerName: string) => void; onError?: (errorMessage: string) => void; onVoted?: (userInfo: VoteInfo) => void; web3Wrapper?: Web3Wrapper; contractWrappers?: ContractWrappers; currentBalance?: BigNumber; selectedAddress: string; isLedger: boolean; injectedProvider?: InjectedProvider; ledgerSubproviderIfExists?: LedgerSubprovider; provider?: ZeroExProvider; zeipId: number; } interface State { isWalletConnected: boolean; isSubmitting: boolean; isSuccessful: boolean; isAwaitingLedgerSignature: boolean; isVoted: boolean; selectedAddress?: string; votePreference?: string; voteHash?: string; signedVote?: SignedVote; comment?: string; errorMessage?: string; errors: ErrorProps; } interface SignedVote { signature: string; from: string; zeip: number; preference: string; } interface FormProps { isSuccessful?: boolean; isSubmitting?: boolean; } interface ErrorProps { [key: string]: string; } // This is a copy of the generic form and includes a number of extra fields // TODO remove the extraneous fields export class VoteForm extends React.Component { public static defaultProps = { currentBalance: new BigNumber(0), isWalletConnected: false, isSubmitting: false, isSuccessful: false, isLedger: false, isVoted: false, errors: {}, }; public networkId: number; public state: State = { isWalletConnected: false, isAwaitingLedgerSignature: false, isSubmitting: false, isSuccessful: false, isVoted: false, votePreference: null, errors: {}, }; // shared fields public commentsRef: React.RefObject = React.createRef(); public constructor(props: Props) { super(props); } public render(): React.ReactNode { const { votePreference, errors, isSuccessful, isAwaitingLedgerSignature } = this.state; const { currentBalance, selectedAddress, zeipId } = this.props; const bigNumberFormat = { decimalSeparator: '.', groupSeparator: ',', groupSize: 3, secondaryGroupSize: 0, fractionGroupSeparator: ' ', fractionGroupSize: 0, }; const formattedBalance = Web3Wrapper.toUnitAmount(currentBalance, constants.DECIMAL_PLACES_ETH).toFormat( 0, BigNumber.ROUND_FLOOR, bigNumberFormat, ); return (
ZEIP-{zeipId} Vote Make sure you are informed to the best of your ability before casting your vote. It will have lasting implications for the 0x ecosystem. Voting address: {selectedAddress}
Voting balance: {formattedBalance} ZRX
{errors.signError !== undefined && ( {errors.signError} )} Submit
); } private readonly _createAndSubmitVoteAsync = async (e: React.FormEvent): Promise => { e.preventDefault(); const { votePreference, comment } = this.state; const { currentBalance, selectedAddress, isLedger, zeipId } = this.props; const makerAddress = selectedAddress; if (isLedger) { this.setState({ isAwaitingLedgerSignature: true }); } const domainType = [{ name: 'name', type: 'string' }]; const voteType = [ { name: 'preference', type: 'string' }, { name: 'zeip', type: 'uint256' }, { name: 'from', type: 'address' }, ]; const domainData = { name: '0x Protocol Governance', }; const message = { zeip: zeipId, preference: votePreference, from: makerAddress, }; const typedData = { types: { EIP712Domain: domainType, Vote: voteType, }, domain: domainData, message, primaryType: 'Vote', }; const voteHashBuffer = signTypedDataUtils.generateTypedDataHash(typedData); const voteHashHex = `0x${voteHashBuffer.toString('hex')}`; try { const signedVote = await this._signVoteAsync(makerAddress, typedData); // Store the signed vote this.setState(prevState => ({ ...prevState, signedVote, voteHash: voteHashHex, isSuccessful: true, isAwaitingLedgerSignature: false, })); const voteDomain = utils.isProduction() ? `https://${configs.DOMAIN_VOTE}` : `https://${configs.DOMAIN_VOTE}/staging`; const voteEndpoint = `${voteDomain}/v1/vote`; const requestBody = { ...signedVote, comment }; const response = await fetch(voteEndpoint, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); if (response.ok) { if (this.props.onVoted) { this.props.onVoted({ userBalance: currentBalance, voteValue: this._getVoteValueFromString(votePreference), }); } } else { const responseBody = await response.json(); const errorMessage = responseBody.reason !== undefined ? responseBody.reason : 'Unknown Error'; this._handleError(errorMessage); } } catch (err) { this._handleError(err.message); } }; private _handleError(errorMessage: string): void { const { onError } = this.props; onError ? onError(errorMessage) : this.setState({ errors: { signError: errorMessage, }, isSuccessful: false, isAwaitingLedgerSignature: false, }); this.setState({ isAwaitingLedgerSignature: false, }); } private async _signVoteAsync(signerAddress: string, typedData: any): Promise { const { provider: providerEngine } = this.props; let signatureHex; try { signatureHex = await this._eip712SignatureAsync(signerAddress, typedData); } catch (err) { // HACK: We are unable to handle specific errors thrown since provider is not an object // under our control. It could be Metamask Web3, Ethers, or any general RPC provider. // We check for a user denying the signature request in a way that supports Metamask and // Coinbase Wallet. Unfortunately for signers with a different error message, // they will receive two signature requests. if (err.message.includes('User denied message signature')) { throw err; } const voteHashBuffer = signTypedDataUtils.generateTypedDataHash(typedData); const voteHashHex = `0x${voteHashBuffer.toString('hex')}`; signatureHex = await signatureUtils.ecSignHashAsync(providerEngine, voteHashHex, signerAddress); } const signedVote = { ...typedData.message, signature: signatureHex }; return signedVote; } private readonly _eip712SignatureAsync = async (address: string, typedData: any): Promise => { const signature = await this.props.web3Wrapper.signTypedDataAsync(address, typedData); const ecSignatureRSV = this._parseSignatureHexAsRSV(signature); const signatureBuffer = Buffer.concat([ ethUtil.toBuffer(ecSignatureRSV.v), ethUtil.toBuffer(ecSignatureRSV.r), ethUtil.toBuffer(ecSignatureRSV.s), ethUtil.toBuffer(SignatureType.EIP712), ]); const signatureHex = `0x${signatureBuffer.toString('hex')}`; return signatureHex; }; private _parseSignatureHexAsRSV(signatureHex: string): ECSignature { const { v, r, s } = ethUtil.fromRpcSig(signatureHex); const ecSignature: ECSignature = { v, r: ethUtil.bufferToHex(r), s: ethUtil.bufferToHex(s), }; return ecSignature; } private _setVotePreference(e: React.ChangeEvent): void { this.setState({ votePreference: e.currentTarget.value, }); } private _setVoteComment(e: React.ChangeEvent): void { this.setState({ comment: e.currentTarget.value, }); } private _getVoteValueFromString(value: string): VoteValue { return VoteValue.Yes === value ? VoteValue.Yes : VoteValue.No; } } const InputRow = styled.div` width: 100%; flex: 0 0 auto; @media (min-width: 768px) { display: flex; justify-content: space-between; margin-bottom: 30px; } `; const ButtonRow = styled(InputRow)` position: relative; @media (max-width: 768px) { display: flex; flex-direction: column; button:nth-child(1) { order: 2; } button:nth-child(2) { order: 1; margin-bottom: 10px; } } `; const ButtonDisabled = styled(Button)<{ isDisabled?: boolean; disabled?: boolean }>` background-color: ${props => props.disabled && '#898990'}; opacity: ${props => props.disabled && '0.4'}; `; const Form = styled.form` position: relative; transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out; opacity: ${props => props.isSuccessful && `0`}; visibility: ${props => props.isSuccessful && `hidden`}; `; const PreferenceWrapper = styled.div` margin-bottom: 30px; `;