diff --git a/assets/images/connect_ledger.svg b/assets/images/connect_ledger.svg deleted file mode 100644 index 09060972..00000000 --- a/assets/images/connect_ledger.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/assets/images/connect_ledger_dark.svg b/assets/images/connect_ledger_dark.svg new file mode 100644 index 00000000..f9fb0a6d --- /dev/null +++ b/assets/images/connect_ledger_dark.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/images/connect_ledger_light.svg b/assets/images/connect_ledger_light.svg new file mode 100644 index 00000000..13b33723 --- /dev/null +++ b/assets/images/connect_ledger_light.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/images/enter_passphase_dark.svg b/assets/images/enter_passphase_dark.svg new file mode 100644 index 00000000..327003af --- /dev/null +++ b/assets/images/enter_passphase_dark.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/images/enter_passphase_light.svg b/assets/images/enter_passphase_light.svg new file mode 100644 index 00000000..ea533709 --- /dev/null +++ b/assets/images/enter_passphase_light.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/images/private_key_dark.svg b/assets/images/private_key_dark.svg new file mode 100644 index 00000000..f4307731 --- /dev/null +++ b/assets/images/private_key_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/private_key_light.svg b/assets/images/private_key_light.svg new file mode 100644 index 00000000..b8286360 --- /dev/null +++ b/assets/images/private_key_light.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/terra-station.svg b/assets/images/terra-station.svg index 0ae05944..153ed256 100644 --- a/assets/images/terra-station.svg +++ b/assets/images/terra-station.svg @@ -1,4 +1,9 @@ - - - + + + + + + + + diff --git a/assets/images/upload_proof_dark.svg b/assets/images/upload_proof_dark.svg new file mode 100644 index 00000000..c136da58 --- /dev/null +++ b/assets/images/upload_proof_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/upload_proof_light.svg b/assets/images/upload_proof_light.svg new file mode 100644 index 00000000..3dfb03ae --- /dev/null +++ b/assets/images/upload_proof_light.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/use_phrase.svg b/assets/images/use_phrase.svg deleted file mode 100644 index 543f254c..00000000 --- a/assets/images/use_phrase.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/components/ConnectChainDialog/SelectWalletType.tsx b/components/ConnectChainDialog/SelectWalletType.tsx index 18d41a11..7a0a26a6 100644 --- a/components/ConnectChainDialog/SelectWalletType.tsx +++ b/components/ConnectChainDialog/SelectWalletType.tsx @@ -4,27 +4,36 @@ import { DialogContentText, Typography, Box, - Grid, useTheme, } from '@material-ui/core' import useTranslation from 'next-translate/useTranslation' import React from 'react' -import ConnectLedgerIcon from '../../assets/images/connect_ledger.svg' -import UsePhraseIcon from '../../assets/images/use_phrase.svg' +import ConnectLedgerIconLight from '../../assets/images/connect_ledger_light.svg' +import ConnectLedgerIconDark from '../../assets/images/connect_ledger_dark.svg' +import UsePhraseIconLight from '../../assets/images/enter_passphase_light.svg' +import UsePhraseIconDark from '../../assets/images/enter_passphase_dark.svg' +import PrivateKeyIconLight from '../../assets/images/private_key_light.svg' +import PrivateKeyIconDark from '../../assets/images/private_key_dark.svg' +import UploadProofIconLight from '../../assets/images/upload_proof_light.svg' +import UploadProofIconDark from '../../assets/images/upload_proof_dark.svg' import KeplrIcon from '../../assets/images/keplr.svg' import TerraStationIcon from '../../assets/images/terra-station.svg' import useStyles from './styles' +import { useGeneralContext } from '../../contexts/GeneralContext' interface SelectWalletTypeProps { chain: string - onConfirm(type: 'mnemonic' | 'ledger' | 'keplr' | 'terra station'): void + onConfirm( + type: 'mnemonic' | 'ledger' | 'private key' | 'upload proof' | 'keplr' | 'terra station' + ): void error: string } const SelectWalletType: React.FC = ({ onConfirm, error, chain }) => { const { t } = useTranslation('common') const classes = useStyles() - const theme = useTheme() + const themeStyle = useTheme() + const { theme } = useGeneralContext() const isTerra = chain === 'terra' @@ -32,53 +41,39 @@ const SelectWalletType: React.FC = ({ onConfirm, error, c {t('select connect chain method')} - - - onConfirm('mnemonic')}> - - - - - {t('use recovery phrase')} - - - - - onConfirm('ledger')}> - - - - - {t('connect with ledger')} - - - - - onConfirm('keplr')}> - - - - - {t('connect with keplr')} - - - - {isTerra ? ( - - onConfirm('terra station')} - > - - - - - {t('connect with terra station')} - - - - ) : null} - + onConfirm('mnemonic')}> + {theme === 'light' ? : } + {t('use recovery phrase')} + + onConfirm('private key')}> + {theme === 'light' ? : } + {t('use private key')} + + onConfirm('ledger')}> + + {theme === 'light' ? : } + + {t('connect with ledger')} + + onConfirm('upload proof')}> + {theme === 'light' ? : } + {t('upload chain link proof')} + + onConfirm('keplr')}> + + + + {t('connect with keplr')} + + + {isTerra ? ( + onConfirm('terra station')}> + + + + {t('connect with terra station')} + + ) : null} {error ? {error} : null} diff --git a/components/ConnectChainDialog/TextInputDialogContent.tsx b/components/ConnectChainDialog/TextInputDialogContent.tsx new file mode 100644 index 00000000..0597e4d4 --- /dev/null +++ b/components/ConnectChainDialog/TextInputDialogContent.tsx @@ -0,0 +1,68 @@ +import { Button, DialogActions, DialogContent, Typography, Box, TextField } from '@material-ui/core' +import useTranslation from 'next-translate/useTranslation' +import React from 'react' +import useStyles from './styles' + +interface TextInputDialogContentProps { + onConfirm(text: string): void + error: string + title: string + placeholder: string +} + +const TextInputDialogContent: React.FC = ({ + onConfirm, + error, + title, + placeholder, +}) => { + const { t } = useTranslation('common') + const classes = useStyles() + const [text, setText] = React.useState('') + + return ( +
{ + e.preventDefault() + onConfirm(text) + }} + > + + {title} + setText(e.target.value)} + /> + + + {error} + + + + + + + + +
+ ) +} + +export default TextInputDialogContent diff --git a/components/ConnectChainDialog/index.tsx b/components/ConnectChainDialog/index.tsx index 90e8cc0e..0c3ff1fc 100644 --- a/components/ConnectChainDialog/index.tsx +++ b/components/ConnectChainDialog/index.tsx @@ -1,6 +1,8 @@ +/* eslint-disable import/no-extraneous-dependencies */ import { Dialog, DialogTitle, IconButton } from '@material-ui/core' import useTranslation from 'next-translate/useTranslation' import React from 'react' +import { pubkeyToAddress } from '@cosmjs/amino' import CloseIcon from '../../assets/images/icons/icon_cross.svg' import BackIcon from '../../assets/images/icons/icon_back.svg' import useStyles from './styles' @@ -19,6 +21,7 @@ import connectableChains from '../../misc/connectableChains' import generateProof from '../../misc/tx/generateProof' import SelectLedgerApp from './SelectLedgerApp' import useSendTransaction from '../../misc/tx/useSendTransaction' +import TextInputDialogContent from './TextInputDialogContent' let ledgerTransport @@ -30,6 +33,8 @@ enum Stage { SelectLedgerAppStage = 'select ledger app', SelectAddressStage = 'select address', ConnectLedgerStage = 'connect ledger', + EnterPrivateKeyStage = 'enter private key', + EnterChainLinkProofStage = 'enter chain link proof', SignInLedgerStage = 'sign in ledger', } @@ -94,13 +99,16 @@ const ConnectChainDialog: React.FC = ({ async ( info?: { account: number; change: number; index: number; address: string }, isKeplr?: boolean, - isTerraStation?: boolean + isTerraStation?: boolean, + privateKey?: string, + proofText?: string ) => { try { setError('') + const { proof, address } = await generateProof( account.address, - mnemonic, + privateKey || mnemonic, { prefix: connectableChains[chain].prefix, coinType: connectableChains[ledgerApp || chain].coinType, @@ -110,11 +118,14 @@ const ConnectChainDialog: React.FC = ({ index: info ? info.index : 0, chainId: connectableChains[chain].chainId, feeDenom: connectableChains[chain].feeDenom, + isPrivateKey: !!privateKey, }, ledgerTransport, isKeplr, - isTerraStation + isTerraStation, + proofText ) + await sendTransaction(password, account.address, { msgs: [ { @@ -164,7 +175,7 @@ const ConnectChainDialog: React.FC = ({ case Stage.SelectWalletTypeStage: return { title: t('connect chain'), - dialogSize: chain === 'terra' ? 'lg' : 'md', + dialogSize: 'sm', content: ( = ({ genProofAndSendTx(undefined, true) } else if (type === 'terra station') { genProofAndSendTx(undefined, false, true) + } else if (type === 'private key') { + setStage(Stage.EnterPrivateKeyStage) + } else if (type === 'upload proof') { + setStage(Stage.EnterChainLinkProofStage) + } else if (type === 'mnemonic') { + setStage(Stage.ImportMnemonicPhraseStage) } else { - setStage( - type === 'mnemonic' - ? Stage.ImportMnemonicPhraseStage - : Stage.SelectLedgerAppStage - ) + setStage(Stage.SelectLedgerAppStage) } }} /> @@ -234,6 +247,30 @@ const ConnectChainDialog: React.FC = ({ /> ), } + case Stage.EnterChainLinkProofStage: + case Stage.EnterPrivateKeyStage: + return { + title: t( + stage === Stage.EnterChainLinkProofStage ? 'chain link proof' : 'import private key' + ), + dialogSize: 'sm', + content: ( + { + if (stage === Stage.EnterChainLinkProofStage) { + genProofAndSendTx(undefined, undefined, undefined, undefined, text) + } else { + genProofAndSendTx(undefined, undefined, undefined, text) + } + }} + /> + ), + } case Stage.SelectAddressStage: return { title: t('select address'), diff --git a/components/ConnectChainDialog/styles.ts b/components/ConnectChainDialog/styles.ts index cbef97c7..9f22e452 100644 --- a/components/ConnectChainDialog/styles.ts +++ b/components/ConnectChainDialog/styles.ts @@ -20,12 +20,13 @@ const useStyles = makeStyles( selectionBox: { border: `1px solid ${theme.palette.grey[200]}`, borderRadius: theme.shape.borderRadius, - height: theme.spacing(32), padding: theme.spacing(2, 4), + marginBottom: theme.spacing(2), display: 'flex', - flexDirection: 'column', + flexDirection: 'row', width: '100%', - justifyContent: 'center', + justifyContent: 'flex-start', + alignItems: 'center', '&:hover': { border: `1px solid ${theme.palette.grey[300]}`, }, diff --git a/contexts/GeneralContext.tsx b/contexts/GeneralContext.tsx index 344663dd..0a090b90 100644 --- a/contexts/GeneralContext.tsx +++ b/contexts/GeneralContext.tsx @@ -15,10 +15,8 @@ interface GeneralState { favAddresses: FavAddress[] setCurrency?: React.Dispatch> setTheme?: React.Dispatch> - setFavValidators?: React.Dispatch> addFavValidators?: (id: string) => void deleteFavValidators?: (id: string) => void - setFavAddresses?: React.Dispatch> addFavAddresses?: (n: FavAddress) => void deleteFavAddresses?: (id: string) => void updateFavAddresses?: (n: FavAddress) => void @@ -108,11 +106,9 @@ const GeneralProvider: React.FC = ({ children }) => { alwaysRequirePassword, setAlwaysRequirePassword, favValidators, - setFavValidators, addFavValidators, deleteFavValidators, favAddresses, - setFavAddresses, addFavAddresses, deleteFavAddresses, updateFavAddresses, diff --git a/locales/en/common.json b/locales/en/common.json index 61d3d20d..982bbd52 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -492,5 +492,12 @@ "connect account description": "Please make sure you have connected the correct eligible account", "connected accounts": "Connected accounts", "claim more description": "If you want to claim more, please connect the following accounts", - "not eligible for airdrop": "This account is not eligible to receive the airdrop" + "not eligible for airdrop": "This account is not eligible to receive the airdrop", + "upload chain link proof": "Upload Chain Link Proof", + "use private key": "Use Private Key", + "import private key": "Import Private Key", + "chain link proof": "Chain Link Proof", + "private key": "Private Key", + "proof": "Proof", + "proof description": "The proof generated by other app" } diff --git a/misc/tx/generateProof.ts b/misc/tx/generateProof.ts index 06d06f19..eb096568 100644 --- a/misc/tx/generateProof.ts +++ b/misc/tx/generateProof.ts @@ -1,5 +1,5 @@ /* eslint-disable import/no-extraneous-dependencies */ -import { Secp256k1HdWallet, serializeSignDoc } from '@cosmjs/amino' +import { Secp256k1HdWallet, Secp256k1Wallet, serializeSignDoc } from '@cosmjs/amino' import { stringToPath } from '@cosmjs/crypto' import { LedgerSigner } from '@cosmjs/ledger-amino' import { toBase64 } from '@cosmjs/encoding' @@ -49,12 +49,32 @@ const signProofWithTerraStation = (tx) => const generateProof = async ( signerAddress: string, - mnemonic: string, + mnemonicOrPrivateKey: string, option: WalletOption, ledgerTransport?: any, isKeplr?: boolean, - isTerraStation?: boolean + isTerraStation?: boolean, + proofText?: string ): Promise<{ proof: ChainLinkProof; address: string }> => { + // Raw proof text given + if (proofText) { + try { + const proofObj = JSON.parse(proofText) + return { + proof: { + plainText: proofObj.proof.plain_text, + pubKey: { + typeUrl: '/cosmos.crypto.secp256k1.PubKey', + value: proofObj.proof.pub_key.value, + }, + signature: proofObj.proof.signature, + }, + address: proofObj.address.value, + } + } catch (err) { + throw new Error('Invalid Proof') + } + } const proof = { account_number: '0', chain_id: option.chainId, @@ -72,6 +92,26 @@ const generateProof = async ( sequence: '0', } + if (option.isPrivateKey) { + const signer = await Secp256k1Wallet.fromKey( + Buffer.from(mnemonicOrPrivateKey, 'hex'), + option.prefix + ) + const [ac] = await signer.getAccounts() + const { signature } = await signer.signAmino(ac.address, proof) + return { + proof: { + plainText: Buffer.from(JSON.stringify(proof, null, 0)).toString('hex'), + pubKey: { + typeUrl: '/cosmos.crypto.secp256k1.PubKey', + value: toBase64(ac.pubkey), + }, + signature: Buffer.from(signature.signature, 'base64').toString('hex'), + }, + address: ac.address, + } + } + if (isKeplr) { if (!window.keplr) { throw new Error('no keplr') @@ -173,7 +213,7 @@ const generateProof = async ( } let signer if (!ledgerTransport) { - signer = await Secp256k1HdWallet.fromMnemonic(mnemonic, signerOptions) + signer = await Secp256k1HdWallet.fromMnemonic(mnemonicOrPrivateKey, signerOptions) } else { signer = new LedgerSigner(ledgerTransport, { ...signerOptions, diff --git a/misc/tx/getWalletAddress.ts b/misc/tx/getWalletAddress.ts index 0f22e5a0..b1ae34b5 100644 --- a/misc/tx/getWalletAddress.ts +++ b/misc/tx/getWalletAddress.ts @@ -1,21 +1,22 @@ -import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing' +import { DirectSecp256k1HdWallet, DirectSecp256k1Wallet } from '@cosmjs/proto-signing' import { stringToPath } from '@cosmjs/crypto' import { LedgerSigner } from '@cosmjs/ledger-amino' import TerraApp from '@terra-money/ledger-terra-js' export interface WalletOption { - coinType: number - account: number - change: number - index: number + coinType?: number + account?: number + change?: number + index?: number prefix: string - ledgerAppName: string + ledgerAppName?: string chainId?: string feeDenom?: string + isPrivateKey?: boolean } const getWalletAddress = async ( - mnemonic: string, + mnemonicOrPrivateKey: string, option: WalletOption, ledgerTransport?: any, showAddressOnLedger?: boolean @@ -25,16 +26,32 @@ const getWalletAddress = async ( // const address = getPubkeyFromConfig(new SignerConfig('', mnemonic, '')) // return address // } + let signer + // Generate by Private Key + if (!ledgerTransport && option.isPrivateKey) { + signer = await DirectSecp256k1Wallet.fromKey( + Buffer.from(mnemonicOrPrivateKey, 'hex'), + option.prefix + ) + const accounts = await signer.getAccounts() + return accounts[0].address + } if (option.ledgerAppName === 'terra' && ledgerTransport) { const app = new TerraApp(ledgerTransport) - const hdPath = [44, option.coinType, option.account || 0, option.change || 0, option.index || 0] + const hdPath = [ + 44, + option.coinType || 118, + option.account || 0, + option.change || 0, + option.index || 0, + ] const result = await app.getAddressAndPubKey(hdPath, option.prefix) if (showAddressOnLedger) { await app.showAddressAndPubKey(hdPath, option.prefix) } return result.bech32_address } - let signer + const signerOptions = { hdPaths: [ stringToPath( @@ -45,8 +62,8 @@ const getWalletAddress = async ( ], prefix: option.prefix, } - if (!ledgerTransport) { - signer = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, signerOptions) + if (!ledgerTransport && !option.isPrivateKey) { + signer = await DirectSecp256k1HdWallet.fromMnemonic(mnemonicOrPrivateKey, signerOptions) } else { signer = new LedgerSigner(ledgerTransport, { ...signerOptions, diff --git a/package.json b/package.json index 6ace5ac5..373acc84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "forbole-x", - "version": "0.16.2", + "version": "0.17.0", "private": true, "scripts": { "dev": "next dev",