Refactor wallet into util, restore from mnemonic, prep for session page

This commit is contained in:
Ilja 2022-02-14 12:10:44 +02:00
parent 4a95f9edbd
commit de05b8c82c
15 changed files with 127 additions and 105 deletions

View File

@ -19,13 +19,13 @@
"react-qr-reader-es6": "2.2.1-2",
"framer-motion": "6.2.6",
"ethers": "5.5.4",
"valtio": "1.2.12"
"valtio": "1.3.0"
},
"devDependencies": {
"@walletconnect/types": "2.0.0-beta.22",
"@types/node": "17.0.17",
"@types/react": "17.0.39",
"eslint": "8.8.0",
"eslint": "8.9.0",
"eslint-config-next": "12.0.10",
"eslint-config-prettier": "8.3.0",
"prettier": "2.5.1",

View File

@ -12,6 +12,7 @@ export default function AccountCard({ name, logo, rgb, address }: Props) {
return (
<Card
bordered
clickable
borderWeight="light"
css={{
borderColor: `rgba(${rgb}, 0.4)`,

View File

@ -3,10 +3,19 @@
* @url https://chainlist.org
*/
export type CHAIN = keyof typeof MAINNET_CHAINS
/**
* Types
*/
export type TChain = keyof typeof MAINNET_CHAINS
/**
* Utilities
*/
export const LOGO_BASE_URL = 'https://blockchain-api.xyz/logos/'
/**
* Chains
*/
export const MAINNET_CHAINS = {
'eip155:1': {
chainId: 1,
@ -33,3 +42,15 @@ export const MAINNET_CHAINS = {
rgb: '44, 55, 75'
}
}
/**
* Methods
*/
export const SIGNING_METHODS = {
PERSONAL_SIGN: 'personal_sign',
SEND_TRANSACTION: 'eth_sendTransaction',
SIGN: 'eth_sign',
SIGN_TRANSACTION: 'eth_signTransaction',
SIGN_TYPED_DATA: 'eth_signTypedData',
SIGN_TYPED_DATA_V4: 'eth_signTypedData_v4'
}

View File

@ -1,5 +1,5 @@
import WalletStore from '@/store/WalletStore'
import { createClient } from '@/utils/WalletConnectUtil'
import { createWalletConnectClient } from '@/utils/WalletConnectUtil'
import { createOrRestoreWallet } from '@/utils/WalletUtil'
import { useCallback, useEffect, useState } from 'react'
export default function useInitialization() {
@ -7,8 +7,8 @@ export default function useInitialization() {
const onInitialize = useCallback(async () => {
try {
WalletStore.createWallet()
await createClient()
createOrRestoreWallet()
await createWalletConnectClient()
setInitialized(true)
} catch (err: unknown) {
alert(err)

View File

@ -1,5 +1,5 @@
import ModalStore from '@/store/ModalStore'
import { client } from '@/utils/WalletConnectUtil'
import { walletConnectClient } from '@/utils/WalletConnectUtil'
import { CLIENT_EVENTS } from '@walletconnect/client'
import { SessionTypes } from '@walletconnect/types'
import { useCallback, useEffect } from 'react'
@ -10,24 +10,24 @@ export default function useWalletConnectEventsManager(initialized: boolean) {
}, [])
const onSessionCreated = useCallback((created: SessionTypes.Created) => {
// TODO show successful feedback here
// TODO show successful connection feedback here
}, [])
const onSessionRequest = useCallback(async (request: SessionTypes.RequestEvent) => {
const requestSession = await client?.session.get(request.topic)
const requestSession = await walletConnectClient.session.get(request.topic)
ModalStore.open('SessionRequestModal', { request, requestSession })
}, [])
useEffect(() => {
if (initialized && client) {
if (initialized) {
// 1. Open session proposal modal for confirmation / rejection
client.on(CLIENT_EVENTS.session.proposal, onSessionProposal)
walletConnectClient.on(CLIENT_EVENTS.session.proposal, onSessionProposal)
// 2. Open session created modal to show success feedback
client.on(CLIENT_EVENTS.session.created, onSessionCreated)
walletConnectClient.on(CLIENT_EVENTS.session.created, onSessionCreated)
// 3. Open rpc request handling modal
client.on(CLIENT_EVENTS.session.request, onSessionRequest)
walletConnectClient.on(CLIENT_EVENTS.session.request, onSessionRequest)
}
}, [initialized, onSessionProposal, onSessionCreated, onSessionRequest])
}

View File

@ -1,17 +1,10 @@
import AccountCard from '@/components/AccountCard'
import PageHeader from '@/components/PageHeader'
import WalletStore from '@/store/WalletStore'
import { MAINNET_CHAINS } from '@/utils/EIP155ChainsUtil'
import { MAINNET_CHAINS } from '@/data/EIP155Data'
import { wallet } from '@/utils/WalletUtil'
import { Fragment } from 'react'
import { useSnapshot } from 'valtio'
export default function HomePage() {
const { wallet } = useSnapshot(WalletStore.state)
if (!wallet) {
return null
}
return (
<Fragment>
<PageHeader>Accounts</PageHeader>

View File

@ -1,5 +1,5 @@
import PageHeader from '@/components/PageHeader'
import WalletStore from '@/store/WalletStore'
import { wallet } from '@/utils/WalletUtil'
import { Card, Divider, Row, Switch, Text } from '@nextui-org/react'
import { Fragment } from 'react'
@ -11,7 +11,7 @@ export default function SettingsPage() {
Mnemonic
</Text>
<Card bordered borderWeight="light">
<Text css={{ fontFamily: '$mono' }}>{WalletStore.state.wallet?.mnemonic.phrase}</Text>
<Text css={{ fontFamily: '$mono' }}>{wallet.mnemonic.phrase}</Text>
</Card>
<Text css={{ color: '$yellow500', marginTop: '$5', textAlign: 'center' }}>

View File

@ -1,6 +1,6 @@
import PageHeader from '@/components/PageHeader'
import QrReader from '@/components/QrReader'
import { client } from '@/utils/WalletConnectUtil'
import { walletConnectClient } from '@/utils/WalletConnectUtil'
import { Button, Input, Loading, Text } from '@nextui-org/react'
import { Fragment, useState } from 'react'
@ -11,7 +11,7 @@ export default function WalletConnectPage() {
async function onConnect(uri: string) {
try {
setLoading(true)
await client?.pair({ uri })
await walletConnectClient.pair({ uri })
} catch (err: unknown) {
alert(err)
} finally {

View File

@ -1,29 +0,0 @@
import { Wallet } from 'ethers'
import { proxy } from 'valtio'
/**
* Types
*/
interface State {
wallet?: Wallet
}
/**
* State
*/
const state = proxy<State>({
wallet: undefined
})
/**
* Store / Actions
*/
const WalletStore = {
state,
createWallet() {
state.wallet = Wallet.createRandom()
}
}
export default WalletStore

View File

@ -1,3 +1,6 @@
/**
* Truncates string (in the middle) via given lenght value
*/
export function truncate(value: string, length: number) {
if (value.length <= length) {
return value
@ -10,3 +13,14 @@ export function truncate(value: string, length: number) {
return value.substring(0, frontLength) + separator + value.substring(value.length - backLength)
}
/**
* Helps to get message from various sign methods present in eth
* @details https://docs.metamask.io/guide/signing-data.html#a-brief-history
*/
export function getSignMessage(params: string[], walletAddress: string) {
// Remove our own address from params, so we are left with message
params.filter(p => p !== walletAddress)
return params[0]
}

View File

@ -1,9 +1,9 @@
import WalletConnectClient from '@walletconnect/client'
export let client: WalletConnectClient | undefined = undefined
export let walletConnectClient: WalletConnectClient
export async function createClient() {
client = await WalletConnectClient.init({
export async function createWalletConnectClient() {
walletConnectClient = await WalletConnectClient.init({
controller: true,
projectId: '8f331b9812e0e5b8f2da2c7203624869',
relayUrl: 'wss://relay.walletconnect.com',

View File

@ -0,0 +1,17 @@
import { Wallet } from 'ethers'
const STORAGE_KEY = 'WALLET_MNEMONIC'
export let wallet: Wallet
export function createOrRestoreWallet() {
const mnemonic = localStorage.getItem(STORAGE_KEY)
if (mnemonic) {
wallet = Wallet.fromMnemonic(mnemonic)
} else {
wallet = Wallet.createRandom()
// Don't store mnemonic in local storage in a production project!
localStorage.setItem(STORAGE_KEY, wallet.mnemonic.phrase)
}
}

View File

@ -1,14 +1,13 @@
import { MAINNET_CHAINS, TChain } from '@/data/EIP155Data'
import ModalStore from '@/store/ModalStore'
import WalletStore from '@/store/WalletStore'
import { CHAIN, MAINNET_CHAINS } from '@/utils/EIP155ChainsUtil'
import { client } from '@/utils/WalletConnectUtil'
import { walletConnectClient } from '@/utils/WalletConnectUtil'
import { wallet } from '@/utils/WalletUtil'
import { Avatar, Button, Col, Container, Divider, Link, Modal, Row, Text } from '@nextui-org/react'
import { Fragment } from 'react'
export default function SessionProposalModal() {
// Get proposal data and wallet address from store
const proposal = ModalStore.state.data?.proposal
const address = WalletStore.state.wallet?.address
// Ensure proposal is defined
if (!proposal) {
@ -24,21 +23,21 @@ export default function SessionProposalModal() {
// Hanlde approve action
async function onApprove() {
if (client && proposal && address) {
if (proposal) {
const response = {
state: {
accounts: chains.map(chain => `${chain}:${address}`)
accounts: chains.map(chain => `${chain}:${wallet.address}`)
}
}
await client.approve({ proposal, response })
await walletConnectClient.approve({ proposal, response })
}
ModalStore.close()
}
// Hanlde reject action
async function onReject() {
if (client && proposal) {
await client.reject({ proposal })
if (proposal) {
await walletConnectClient.reject({ proposal })
}
ModalStore.close()
}
@ -67,7 +66,7 @@ export default function SessionProposalModal() {
<Col>
<Text h5>Blockchains</Text>
<Text color="$gray400">
{chains.map(chain => MAINNET_CHAINS[chain as CHAIN]?.name ?? chain).join(', ')}
{chains.map(chain => MAINNET_CHAINS[chain as TChain]?.name ?? chain).join(', ')}
</Text>
</Col>
</Row>

View File

@ -1,7 +1,7 @@
import { MAINNET_CHAINS, TChain } from '@/data/EIP155Data'
import ModalStore from '@/store/ModalStore'
import WalletStore from '@/store/WalletStore'
import { CHAIN, MAINNET_CHAINS } from '@/utils/EIP155ChainsUtil'
import { client } from '@/utils/WalletConnectUtil'
import { getSignMessage } from '@/utils/HelperUtil'
import { wallet } from '@/utils/WalletUtil'
import { Avatar, Button, Col, Container, Divider, Link, Modal, Row, Text } from '@nextui-org/react'
import { Fragment } from 'react'
@ -9,23 +9,24 @@ export default function SessionRequestModal() {
// Get request and wallet data from store
const request = ModalStore.state.data?.request
const requestSession = ModalStore.state.data?.requestSession
const { wallet } = WalletStore.state
// Ensure request and wallet are defined
if (!request || !requestSession || !wallet) {
if (!request || !requestSession) {
return <Text>Missing request data</Text>
}
// Get required request data
const { chainId } = request
const { method } = request.request
const { method, params } = request.request
const { protocol } = requestSession.relay
const { name, icons, url } = requestSession.peer.metadata
// Handle approve action (logic varies based on request method)
async function onApprove() {
if (client && wallet) {
// TODO figure out how to sign different personal messages correctly with ethers
// Handle sign requests
if (['eth_sign', 'personal_sign'].includes(method)) {
const message = getSignMessage(params, wallet.address)
const signedMessage = wallet.signMessage(message)
}
}
@ -55,7 +56,7 @@ export default function SessionRequestModal() {
<Row>
<Col>
<Text h5>Blockchain</Text>
<Text color="$gray400">{MAINNET_CHAINS[chainId as CHAIN]?.name ?? chainId}</Text>
<Text color="$gray400">{MAINNET_CHAINS[chainId as TChain]?.name ?? chainId}</Text>
</Col>
</Row>

View File

@ -43,14 +43,14 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
"@eslint/eslintrc@^1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318"
integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==
"@eslint/eslintrc@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.1.0.tgz#583d12dbec5d4f22f333f9669f7d0b7c7815b4d3"
integrity sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
espree "^9.2.0"
espree "^9.3.1"
globals "^13.9.0"
ignore "^4.0.6"
import-fresh "^3.2.1"
@ -1465,10 +1465,10 @@ eslint-plugin-react@^7.27.0:
semver "^6.3.0"
string.prototype.matchall "^4.0.6"
eslint-scope@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153"
integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==
eslint-scope@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642"
integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==
dependencies:
esrecurse "^4.3.0"
estraverse "^5.2.0"
@ -1485,17 +1485,22 @@ eslint-visitor-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.2.0:
eslint-visitor-keys@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz#6fbb166a6798ee5991358bc2daa1ba76cc1254a1"
integrity sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==
eslint@8.8.0:
version "8.8.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.8.0.tgz#9762b49abad0cb4952539ffdb0a046392e571a2d"
integrity sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ==
eslint-visitor-keys@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
eslint@8.9.0:
version "8.9.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.9.0.tgz#a2a8227a99599adc4342fd9b854cb8d8d6412fdb"
integrity sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q==
dependencies:
"@eslint/eslintrc" "^1.0.5"
"@eslint/eslintrc" "^1.1.0"
"@humanwhocodes/config-array" "^0.9.2"
ajv "^6.10.0"
chalk "^4.0.0"
@ -1503,10 +1508,10 @@ eslint@8.8.0:
debug "^4.3.2"
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
eslint-scope "^7.1.0"
eslint-scope "^7.1.1"
eslint-utils "^3.0.0"
eslint-visitor-keys "^3.2.0"
espree "^9.3.0"
eslint-visitor-keys "^3.3.0"
espree "^9.3.1"
esquery "^1.4.0"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
@ -1531,14 +1536,14 @@ eslint@8.8.0:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@^9.2.0, espree@^9.3.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.0.tgz#c1240d79183b72aaee6ccfa5a90bc9111df085a8"
integrity sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==
espree@^9.3.1:
version "9.3.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd"
integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==
dependencies:
acorn "^8.7.0"
acorn-jsx "^5.3.1"
eslint-visitor-keys "^3.1.0"
eslint-visitor-keys "^3.3.0"
esquery@^1.4.0:
version "1.4.0"
@ -3116,10 +3121,10 @@ v8-compile-cache@^2.0.3:
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
valtio@1.2.12:
version "1.2.12"
resolved "https://registry.yarnpkg.com/valtio/-/valtio-1.2.12.tgz#14f46f6c90b63c6e4176831c68ab9b729fea38ee"
integrity sha512-TlQkbSma4aAAgs6tQXrvGilMZBVH0q8gbbWxRbo2R43FhkW4lWi45f9jC6gAdJdC40bsNnYyBsYDB9OOKNlqSw==
valtio@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/valtio/-/valtio-1.3.0.tgz#b83cfcca8455940119738659946565d5c0389af8"
integrity sha512-wsE6EDIkt+CNZPNHOxNVzoi026Fyt6ZRT750etZCAvrndcdT3N7Z+SSV4kJQdCwl5gNxsnU4BhP1wFS7cu21oA==
dependencies:
proxy-compare "2.0.2"