Implement solana secret key storage and recovery

This commit is contained in:
Ilja 2022-03-14 14:06:19 +02:00
parent 2b695af9f0
commit 1200b60877
13 changed files with 781 additions and 341 deletions

View File

@ -11,6 +11,7 @@
"@walletconnect/client": "experimental", "@walletconnect/client": "experimental",
"@walletconnect/utils": "experimental", "@walletconnect/utils": "experimental",
"@json-rpc-tools/utils": "1.7.6", "@json-rpc-tools/utils": "1.7.6",
"@solana/web3.js": "1.36.0",
"@nextui-org/react": "1.0.2-beta.4", "@nextui-org/react": "1.0.2-beta.4",
"mnemonic-keyring": "1.4.0", "mnemonic-keyring": "1.4.0",
"next": "12.1.0", "next": "12.1.0",
@ -18,7 +19,7 @@
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-qr-reader-es6": "2.2.1-2", "react-qr-reader-es6": "2.2.1-2",
"framer-motion": "6.2.8", "framer-motion": "6.2.8",
"ethers": "5.5.4", "ethers": "5.6.0",
"valtio": "1.3.1", "valtio": "1.3.1",
"react-code-blocks": "0.0.9-0", "react-code-blocks": "0.0.9-0",
"@cosmjs/proto-signing": "0.27.1", "@cosmjs/proto-signing": "0.27.1",
@ -28,8 +29,8 @@
"devDependencies": { "devDependencies": {
"@walletconnect/types": "experimental", "@walletconnect/types": "experimental",
"@types/node": "17.0.21", "@types/node": "17.0.21",
"@types/react": "17.0.39", "@types/react": "17.0.40",
"eslint": "8.10.0", "eslint": "8.11.0",
"eslint-config-next": "12.1.0", "eslint-config-next": "12.1.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"prettier": "2.5.1", "prettier": "2.5.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1,6 +1,7 @@
import SettingsStore from '@/store/SettingsStore' import SettingsStore from '@/store/SettingsStore'
import { cosmosAddresses } from '@/utils/CosmosWalletUtil' import { cosmosAddresses } from '@/utils/CosmosWalletUtil'
import { eip155Addresses } from '@/utils/EIP155WalletUtil' import { eip155Addresses } from '@/utils/EIP155WalletUtil'
import { solanaAddresses } from '@/utils/SolanaWalletUtil'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
export default function AccountPicker() { export default function AccountPicker() {
@ -11,6 +12,7 @@ export default function AccountPicker() {
SettingsStore.setAccount(account) SettingsStore.setAccount(account)
SettingsStore.setEIP155Address(eip155Addresses[account]) SettingsStore.setEIP155Address(eip155Addresses[account])
SettingsStore.setCosmosAddress(cosmosAddresses[account]) SettingsStore.setCosmosAddress(cosmosAddresses[account])
SettingsStore.setSolanaAddress(solanaAddresses[account])
} }
return ( return (

View File

@ -1,5 +1,6 @@
import { COSMOS_MAINNET_CHAINS, TCosmosChain } from '@/data/COSMOSData' import { COSMOS_MAINNET_CHAINS, TCosmosChain } from '@/data/COSMOSData'
import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data' import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data'
import { SOLANA_MAINNET_CHAINS, TSolanaChain } from '@/data/SolanaData'
import { Col, Divider, Row, Text } from '@nextui-org/react' import { Col, Divider, Row, Text } from '@nextui-org/react'
import { Fragment } from 'react' import { Fragment } from 'react'
@ -26,6 +27,7 @@ export default function RequesDetailsCard({ chains, protocol }: IProps) {
chain => chain =>
EIP155_CHAINS[chain as TEIP155Chain]?.name ?? EIP155_CHAINS[chain as TEIP155Chain]?.name ??
COSMOS_MAINNET_CHAINS[chain as TCosmosChain]?.name ?? COSMOS_MAINNET_CHAINS[chain as TCosmosChain]?.name ??
SOLANA_MAINNET_CHAINS[chain as TSolanaChain]?.name ??
chain chain
) )
.join(', ')} .join(', ')}

View File

@ -0,0 +1,25 @@
/**
* Types
*/
export type TSolanaChain = keyof typeof SOLANA_MAINNET_CHAINS
/**
* Chains
*/
export const SOLANA_MAINNET_CHAINS = {
'solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ': {
chainId: '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ',
name: 'Solana',
logo: '/chain-logos/solana-4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ.png',
rgb: '30, 240, 166',
rpc: ''
}
}
/**
* Methods
*/
export const SOLANA_SIGNING_METHODS = {
SOLANA_SIGN_TRANSACTION: 'solana_signTransaction',
SOLANA_SIGN_MESSAGE: 'solana_signMessage'
}

View File

@ -1,6 +1,7 @@
import SettingsStore from '@/store/SettingsStore' import SettingsStore from '@/store/SettingsStore'
import { createOrRestoreCosmosWallet } from '@/utils/CosmosWalletUtil' import { createOrRestoreCosmosWallet } from '@/utils/CosmosWalletUtil'
import { createOrRestoreEIP155Wallet } from '@/utils/EIP155WalletUtil' import { createOrRestoreEIP155Wallet } from '@/utils/EIP155WalletUtil'
import { createOrRestoreSolanaWallet } from '@/utils/SolanaWalletUtil'
import { createWalletConnectClient } from '@/utils/WalletConnectUtil' import { createWalletConnectClient } from '@/utils/WalletConnectUtil'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
@ -11,9 +12,11 @@ export default function useInitialization() {
try { try {
const { eip155Addresses } = createOrRestoreEIP155Wallet() const { eip155Addresses } = createOrRestoreEIP155Wallet()
const { cosmosAddresses } = await createOrRestoreCosmosWallet() const { cosmosAddresses } = await createOrRestoreCosmosWallet()
const { solanaAddresses } = await createOrRestoreSolanaWallet()
SettingsStore.setEIP155Address(eip155Addresses[0]) SettingsStore.setEIP155Address(eip155Addresses[0])
SettingsStore.setCosmosAddress(cosmosAddresses[0]) SettingsStore.setCosmosAddress(cosmosAddresses[0])
SettingsStore.setSolanaAddress(solanaAddresses[0])
await createWalletConnectClient() await createWalletConnectClient()

View File

@ -0,0 +1,19 @@
import { Keypair } from '@solana/web3.js'
export class Solana {
keypair: Keypair
constructor(keypair: Keypair) {
this.keypair = keypair
}
static init(secretKey?: Uint8Array) {
const keypair = secretKey ? Keypair.fromSecretKey(secretKey) : Keypair.generate()
return new Solana(keypair)
}
public async getAccount() {
return await this.keypair.publicKey.toBase58()
}
}

View File

@ -3,13 +3,14 @@ import AccountPicker from '@/components/AccountPicker'
import PageHeader from '@/components/PageHeader' import PageHeader from '@/components/PageHeader'
import { COSMOS_MAINNET_CHAINS } from '@/data/COSMOSData' import { COSMOS_MAINNET_CHAINS } from '@/data/COSMOSData'
import { EIP155_MAINNET_CHAINS, EIP155_TEST_CHAINS } from '@/data/EIP155Data' import { EIP155_MAINNET_CHAINS, EIP155_TEST_CHAINS } from '@/data/EIP155Data'
import { SOLANA_MAINNET_CHAINS } from '@/data/SolanaData'
import SettingsStore from '@/store/SettingsStore' import SettingsStore from '@/store/SettingsStore'
import { Text } from '@nextui-org/react' import { Text } from '@nextui-org/react'
import { Fragment } from 'react' import { Fragment } from 'react'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
export default function HomePage() { export default function HomePage() {
const { testNets, eip155Address, cosmosAddress } = useSnapshot(SettingsStore.state) const { testNets, eip155Address, cosmosAddress, solanaAddress } = useSnapshot(SettingsStore.state)
return ( return (
<Fragment> <Fragment>
@ -25,6 +26,9 @@ export default function HomePage() {
{Object.values(COSMOS_MAINNET_CHAINS).map(({ name, logo, rgb }) => ( {Object.values(COSMOS_MAINNET_CHAINS).map(({ name, logo, rgb }) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={cosmosAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={cosmosAddress} />
))} ))}
{Object.values(SOLANA_MAINNET_CHAINS).map(({ name, logo, rgb }) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={solanaAddress} />
))}
{testNets ? ( {testNets ? (
<Fragment> <Fragment>

View File

@ -8,6 +8,7 @@ interface State {
account: number account: number
eip155Address: string eip155Address: string
cosmosAddress: string cosmosAddress: string
solanaAddress: string
} }
/** /**
@ -17,7 +18,8 @@ const state = proxy<State>({
testNets: typeof localStorage !== 'undefined' ? Boolean(localStorage.getItem('TEST_NETS')) : true, testNets: typeof localStorage !== 'undefined' ? Boolean(localStorage.getItem('TEST_NETS')) : true,
account: 0, account: 0,
eip155Address: '', eip155Address: '',
cosmosAddress: '' cosmosAddress: '',
solanaAddress: ''
}) })
/** /**
@ -38,6 +40,10 @@ const SettingsStore = {
state.cosmosAddress = cosmosAddresses state.cosmosAddress = cosmosAddresses
}, },
setSolanaAddress(solanaAddress: string) {
state.solanaAddress = solanaAddress
},
toggleTestNets() { toggleTestNets() {
state.testNets = !state.testNets state.testNets = !state.testNets
if (state.testNets) { if (state.testNets) {

View File

@ -83,3 +83,10 @@ export function isEIP155Chain(chain: string) {
export function isCosmosChain(chain: string) { export function isCosmosChain(chain: string) {
return chain.includes('cosmos') return chain.includes('cosmos')
} }
/**
* Check if chain is part of SOLANA standard
*/
export function isSolanaChain(chain: string) {
return chain.includes('solana')
}

View File

@ -0,0 +1,52 @@
import { Solana } from '@/lib/Solana'
export let wallet1: Solana
export let wallet2: Solana
export let solanaWallets: Record<string, Solana>
export let solanaAddresses: string[]
let address1: string
let address2: string
/**
* Utilities
*/
export async function createOrRestoreSolanaWallet() {
const secretKey1 = localStorage.getItem('SOLANA_SECRET_KEY_1')
const secretKey2 = localStorage.getItem('SOLANA_SECRET_KEY_2')
if (secretKey1 && secretKey2) {
const secretArray1: number[] = Object.values(JSON.parse(secretKey1))
const secretArray2: number[] = Object.values(JSON.parse(secretKey2))
wallet1 = Solana.init(Uint8Array.from(secretArray1))
wallet2 = Solana.init(Uint8Array.from(secretArray2))
address1 = await wallet1.getAccount()
address2 = await wallet2.getAccount()
} else {
wallet1 = Solana.init()
wallet2 = Solana.init()
address1 = await wallet1.getAccount()
address2 = await wallet2.getAccount()
// Don't store secretKey in local storage in a production project!
localStorage.setItem(
'SOLANA_SECRET_KEY_1',
JSON.stringify(Array.from(wallet1.keypair.secretKey))
)
localStorage.setItem(
'SOLANA_SECRET_KEY_2',
JSON.stringify(Array.from(wallet2.keypair.secretKey))
)
}
solanaWallets = {
[address1]: wallet1,
[address2]: wallet2
}
solanaAddresses = Object.keys(solanaWallets)
return {
solanaWallets,
solanaAddresses
}
}

View File

@ -5,10 +5,12 @@ import RequestMethodCard from '@/components/RequestMethodCard'
import RequestModalContainer from '@/components/RequestModalContainer' import RequestModalContainer from '@/components/RequestModalContainer'
import { COSMOS_MAINNET_CHAINS, TCosmosChain } from '@/data/COSMOSData' import { COSMOS_MAINNET_CHAINS, TCosmosChain } from '@/data/COSMOSData'
import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data' import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data'
import { SOLANA_MAINNET_CHAINS, TSolanaChain } from '@/data/SolanaData'
import ModalStore from '@/store/ModalStore' import ModalStore from '@/store/ModalStore'
import { cosmosAddresses } from '@/utils/CosmosWalletUtil' import { cosmosAddresses } from '@/utils/CosmosWalletUtil'
import { eip155Addresses } from '@/utils/EIP155WalletUtil' import { eip155Addresses } from '@/utils/EIP155WalletUtil'
import { isCosmosChain, isEIP155Chain } from '@/utils/HelperUtil' import { isCosmosChain, isEIP155Chain, isSolanaChain } from '@/utils/HelperUtil'
import { solanaAddresses } from '@/utils/SolanaWalletUtil'
import { walletConnectClient } from '@/utils/WalletConnectUtil' import { walletConnectClient } from '@/utils/WalletConnectUtil'
import { Button, Col, Divider, Modal, Row, Text } from '@nextui-org/react' import { Button, Col, Divider, Modal, Row, Text } from '@nextui-org/react'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
@ -16,6 +18,7 @@ import { Fragment, useState } from 'react'
export default function SessionProposalModal() { export default function SessionProposalModal() {
const [selectedEIP155, setSelectedEip155] = useState<string[]>([]) const [selectedEIP155, setSelectedEip155] = useState<string[]>([])
const [selectedCosmos, setSelectedCosmos] = useState<string[]>([]) const [selectedCosmos, setSelectedCosmos] = useState<string[]>([])
const [selectedSolana, setSelectedSolana] = useState<string[]>([])
// Get proposal data and wallet address from store // Get proposal data and wallet address from store
const proposal = ModalStore.state.data?.proposal const proposal = ModalStore.state.data?.proposal
@ -50,6 +53,16 @@ export default function SessionProposalModal() {
} }
} }
// Add / remove address from Solana selection
function onSelectSolana(address: string) {
if (selectedSolana.includes(address)) {
const newAddresses = selectedSolana.filter(a => a !== address)
setSelectedSolana(newAddresses)
} else {
setSelectedSolana([...selectedSolana, address])
}
}
// Hanlde approve action // Hanlde approve action
async function onApprove() { async function onApprove() {
if (proposal) { if (proposal) {
@ -130,6 +143,29 @@ export default function SessionProposalModal() {
</Row> </Row>
</Fragment> </Fragment>
) )
} else if (isSolanaChain(chain)) {
return (
<Fragment>
<Divider y={2} />
<Row>
<Col>
<Text h5>
{`Select ${SOLANA_MAINNET_CHAINS[chain as TSolanaChain].name} Accounts`}
</Text>
{solanaAddresses.map((address, index) => (
<AccountSelectCard
key={address}
address={address}
index={index}
onSelect={() => onSelectSolana(`${chain}:${address}`)}
selected={selectedSolana.includes(`${chain}:${address}`)}
/>
))}
</Col>
</Row>
</Fragment>
)
} }
})} })}
</RequestModalContainer> </RequestModalContainer>

File diff suppressed because it is too large Load Diff