Merge branch 'wallets'

This commit is contained in:
Ilja 2022-03-15 13:30:51 +02:00
commit 3f4a6d5253
12 changed files with 255 additions and 19 deletions

View File

@ -20,6 +20,7 @@
"react-qr-reader-es6": "2.2.1-2",
"framer-motion": "6.2.8",
"ethers": "5.6.0",
"bs58": "5.0.0",
"valtio": "1.3.1",
"react-code-blocks": "0.0.9-0",
"@cosmjs/proto-signing": "0.27.1",

View File

@ -3,6 +3,7 @@ import SessionProposalModal from '@/views/SessionProposalModal'
import SessionSendTransactionModal from '@/views/SessionSendTransactionModal'
import SessionSignCosmosModal from '@/views/SessionSignCosmosModal'
import SessionRequestModal from '@/views/SessionSignModal'
import SessionSignSolanaModal from '@/views/SessionSignSolanaModal'
import SessionSignTypedDataModal from '@/views/SessionSignTypedDataModal'
import SessionUnsuportedMethodModal from '@/views/SessionUnsuportedMethodModal'
import { Modal as NextModal } from '@nextui-org/react'
@ -19,6 +20,7 @@ export default function Modal() {
{view === 'SessionSendTransactionModal' && <SessionSendTransactionModal />}
{view === 'SessionUnsuportedMethodModal' && <SessionUnsuportedMethodModal />}
{view === 'SessionSignCosmosModal' && <SessionSignCosmosModal />}
{view === 'SessionSignSolanaModal' && <SessionSignSolanaModal />}
</NextModal>
)
}

View File

@ -32,6 +32,6 @@ export const SOLANA_CHAINS = { ...SOLANA_MAINNET_CHAINS, ...SOLANA_TEST_CHAINS }
* Methods
*/
export const SOLANA_SIGNING_METHODS = {
SOLANA_SIGN_TRANSACTION: 'solana_signTransaction',
SOLANA_SIGN_MESSAGE: 'solana_signMessage'
SOLANA_SIGN_TRANSACTION: 'sol_signTransaction',
SOLANA_SIGN_MESSAGE: 'sol_signMessage'
}

View File

@ -1,5 +1,6 @@
import { COSMOS_SIGNING_METHODS } from '@/data/COSMOSData'
import { EIP155_SIGNING_METHODS } from '@/data/EIP155Data'
import { SOLANA_SIGNING_METHODS } from '@/data/SolanaData'
import ModalStore from '@/store/ModalStore'
import { walletConnectClient } from '@/utils/WalletConnectUtil'
import { CLIENT_EVENTS } from '@walletconnect/client'
@ -45,6 +46,10 @@ export default function useWalletConnectEventsManager(initialized: boolean) {
case COSMOS_SIGNING_METHODS.COSMOS_SIGN_AMINO:
return ModalStore.open('SessionSignCosmosModal', { requestEvent, requestSession })
case SOLANA_SIGNING_METHODS.SOLANA_SIGN_MESSAGE:
case SOLANA_SIGNING_METHODS.SOLANA_SIGN_TRANSACTION:
return ModalStore.open('SessionSignSolanaModal', { requestEvent, requestSession })
default:
return ModalStore.open('SessionUnsuportedMethodModal', { requestEvent, requestSession })
}

View File

@ -0,0 +1,65 @@
import { Secp256k1Wallet, StdSignDoc } from '@cosmjs/amino'
import { fromHex } from '@cosmjs/encoding'
import { DirectSecp256k1Wallet, makeSignBytes } from '@cosmjs/proto-signing'
// @ts-expect-error
import { SignDoc } from '@cosmjs/proto-signing/build/codec/cosmos/tx/v1beta1/tx'
import Keyring from 'mnemonic-keyring'
/**
* Constants
*/
const DEFAULT_PATH = "m/44'/118'/0'/0/0"
const DEFAULT_PREFIX = 'cosmos'
/**
* Types
*/
interface IInitArguments {
mnemonic?: string
path?: string
prefix?: string
}
/**
* Library
*/
export default class CosmosLib {
private keyring: Keyring
private directSigner: DirectSecp256k1Wallet
private aminoSigner: Secp256k1Wallet
constructor(keyring: Keyring, directSigner: DirectSecp256k1Wallet, aminoSigner: Secp256k1Wallet) {
this.directSigner = directSigner
this.keyring = keyring
this.aminoSigner = aminoSigner
}
static async init({ mnemonic, path, prefix }: IInitArguments) {
const keyring = await Keyring.init({ mnemonic: mnemonic ?? Keyring.generateMnemonic() })
const privateKey = fromHex(keyring.getPrivateKey(path ?? DEFAULT_PATH))
const directSigner = await DirectSecp256k1Wallet.fromKey(privateKey, prefix ?? DEFAULT_PREFIX)
const aminoSigner = await Secp256k1Wallet.fromKey(privateKey, prefix ?? DEFAULT_PREFIX)
return new CosmosLib(keyring, directSigner, aminoSigner)
}
public getMnemonic() {
return this.keyring.mnemonic
}
public async getAccount() {
const account = await this.directSigner.getAccounts()
return account[0]
}
public async signDirect(address: string, signDoc: SignDoc) {
const signDocBytes = makeSignBytes(signDoc)
// @ts-expect-error
return await this.directSigner.signDirect(address, signDocBytes)
}
public async signAmino(address: string, signDoc: StdSignDoc) {
return await this.aminoSigner.signAmino(address, signDoc)
}
}

View File

@ -22,7 +22,7 @@ interface IInitArguments {
/**
* Utility
*/
export class Cosmos {
export class EIP155 {
private keyring: MnemonicKeyring
private directSigner: DirectSecp256k1Wallet
private aminoSigner: Secp256k1Wallet
@ -44,7 +44,7 @@ export class Cosmos {
const directSigner = await DirectSecp256k1Wallet.fromKey(privateKey, chainPrefix)
const aminoSigner = await Secp256k1Wallet.fromKey(privateKey, chainPrefix)
return new Cosmos(keyring, directSigner, aminoSigner)
return new EIP155(keyring, directSigner, aminoSigner)
}
public async getAccount(number = 0) {
@ -58,7 +58,6 @@ export class Cosmos {
}
public async signDirect(address: string, signDoc: SignDoc) {
console.log(signDoc)
const signDocBytes = makeSignBytes(signDoc)
// @ts-expect-error
return await this.directSigner.signDirect(address, signDocBytes)

View File

@ -1,4 +1,6 @@
import { Keypair } from '@solana/web3.js'
import { Keypair, PublicKey, Transaction, TransactionInstructionCtorFields } from '@solana/web3.js'
import bs58 from 'bs58'
import nacl from 'tweetnacl'
export class Solana {
keypair: Keypair
@ -16,4 +18,38 @@ export class Solana {
public async getAccount() {
return await this.keypair.publicKey.toBase58()
}
public async signMessage(message: string) {
const signature = nacl.sign.detached(bs58.decode(message), this.keypair.secretKey)
return signature
}
public async signTransaction(
feePayer: string,
recentBlockhash: string,
instructions: TransactionInstructionCtorFields[]
) {
const tx = new Transaction({
feePayer: new PublicKey(feePayer),
recentBlockhash
})
tx.add(
...instructions.map(i => ({
programId: new PublicKey(i.programId),
data: i.data ? Buffer.from(i.data) : Buffer.from([]),
keys: i.keys.map(k => ({
...k,
pubkey: new PublicKey(k.pubkey)
}))
}))
)
await tx.sign(this.keypair)
const { signature } = tx.signatures[tx.signatures.length - 1]
return { signature }
}
}

View File

@ -20,6 +20,7 @@ interface State {
| 'SessionSendTransactionModal'
| 'SessionUnsuportedMethodModal'
| 'SessionSignCosmosModal'
| 'SessionSignSolanaModal'
data?: ModalData
}

View File

@ -1,8 +1,8 @@
import { Cosmos } from '@/lib/Cosmos'
import CosmosLib from '@/lib/CosmosLib'
export let wallet1: Cosmos
export let wallet2: Cosmos
export let cosmosWallets: Record<string, Cosmos>
export let wallet1: CosmosLib
export let wallet2: CosmosLib
export let cosmosWallets: Record<string, CosmosLib>
export let cosmosAddresses: string[]
let address1: string
@ -12,26 +12,26 @@ let address2: string
* Utilities
*/
export async function createOrRestoreCosmosWallet() {
const mnemonic = localStorage.getItem('WALLET_MNEMONIC')
const mnemonic1 = localStorage.getItem('COSMOS_MNEMONIC_1')
const mnemonic2 = localStorage.getItem('COSMOS_MNEMONIC_2')
if (mnemonic) {
wallet1 = await Cosmos.init({ mnemonic, path: "m/44'/118'/0'/0/0" })
wallet2 = await Cosmos.init({ mnemonic, path: "m/44'/118'/0'/0/1" })
if (mnemonic1 && mnemonic2) {
wallet1 = await CosmosLib.init({ mnemonic: mnemonic1 })
wallet2 = await CosmosLib.init({ mnemonic: mnemonic2 })
const account1 = await wallet1.getAccount()
const account2 = await wallet2.getAccount()
address1 = account1.address
address2 = account2.address
} else {
wallet1 = await Cosmos.init({ path: "m/44'/118'/0'/0/0" })
const mnemonic = wallet1.getMnemonic()
// We can reuse same mnemonic for both wallets
wallet2 = await Cosmos.init({ mnemonic, path: "m/44'/118'/0'/0/1" })
wallet1 = await CosmosLib.init({})
wallet2 = await CosmosLib.init({})
const account1 = await wallet1.getAccount()
const account2 = await wallet2.getAccount()
address1 = account1.address
address2 = account2.address
// Don't store mnemonic in local storage in a production project!
localStorage.setItem('WALLET_MNEMONIC', mnemonic)
localStorage.setItem('COSMOS_MNEMONIC_1', wallet1.getMnemonic())
localStorage.setItem('COSMOS_MNEMONIC_2', wallet2.getMnemonic())
}
cosmosWallets = {

View File

@ -0,0 +1,35 @@
import { SOLANA_SIGNING_METHODS } from '@/data/SolanaData'
import { getWalletAddressFromParams } from '@/utils/HelperUtil'
import { solanaAddresses, solanaWallets } from '@/utils/SolanaWalletUtil'
import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils'
import { RequestEvent } from '@walletconnect/types'
import { ERROR } from '@walletconnect/utils'
export async function approveSolanaRequest(requestEvent: RequestEvent) {
const { method, params, id } = requestEvent.request
const wallet = solanaWallets[getWalletAddressFromParams(solanaAddresses, params)]
switch (method) {
case SOLANA_SIGNING_METHODS.SOLANA_SIGN_MESSAGE:
const signedMessage = await wallet.signMessage(params.signDoc)
return formatJsonRpcResult(id, signedMessage)
case SOLANA_SIGNING_METHODS.SOLANA_SIGN_TRANSACTION:
const signedTransaction = await wallet.signTransaction(
params.feePayer,
params.recentBlockhash,
params.instructions
)
return formatJsonRpcResult(id, signedTransaction)
default:
throw new Error(ERROR.UNKNOWN_JSONRPC_METHOD.format().message)
}
}
export function rejectSolanaRequest(request: RequestEvent['request']) {
const { id } = request
return formatJsonRpcError(id, ERROR.JSONRPC_REQUEST_METHOD_REJECTED.format().message)
}

View File

@ -0,0 +1,80 @@
import ProjectInfoCard from '@/components/ProjectInfoCard'
import RequestDataCard from '@/components/RequestDataCard'
import RequesDetailsCard from '@/components/RequestDetalilsCard'
import RequestMethodCard from '@/components/RequestMethodCard'
import RequestModalContainer from '@/components/RequestModalContainer'
import ModalStore from '@/store/ModalStore'
import { approveSolanaRequest, rejectSolanaRequest } from '@/utils/SolanaRequestHandlerUtil'
import { walletConnectClient } from '@/utils/WalletConnectUtil'
import { Button, Divider, Modal, Text } from '@nextui-org/react'
import { Fragment } from 'react'
export default function SessionSignSolanaModal() {
// Get request and wallet data from store
const requestEvent = ModalStore.state.data?.requestEvent
const requestSession = ModalStore.state.data?.requestSession
// Ensure request and wallet are defined
if (!requestEvent || !requestSession) {
return <Text>Missing request data</Text>
}
// Get required request data
const { method, params } = requestEvent.request
// Handle approve action (logic varies based on request method)
async function onApprove() {
if (requestEvent) {
const response = await approveSolanaRequest(requestEvent)
await walletConnectClient.respond({
topic: requestEvent.topic,
response
})
ModalStore.close()
}
}
// Handle reject action
async function onReject() {
if (requestEvent) {
const response = rejectSolanaRequest(requestEvent.request)
await walletConnectClient.respond({
topic: requestEvent.topic,
response
})
ModalStore.close()
}
}
return (
<Fragment>
<RequestModalContainer title="Sign Message">
<ProjectInfoCard metadata={requestSession.peer.metadata} />
<Divider y={2} />
<RequesDetailsCard
chains={[requestEvent.chainId ?? '']}
protocol={requestSession.relay.protocol}
/>
<Divider y={2} />
<RequestDataCard data={params} />
<Divider y={2} />
<RequestMethodCard methods={[method]} />
</RequestModalContainer>
<Modal.Footer>
<Button auto flat color="error" onClick={onReject}>
Reject
</Button>
<Button auto flat color="success" onClick={onApprove}>
Approve
</Button>
</Modal.Footer>
</Fragment>
)
}

View File

@ -1419,6 +1419,11 @@ base-x@^3.0.2:
dependencies:
safe-buffer "^5.0.1"
base-x@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a"
integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==
base64-js@^1.3.0, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -1524,6 +1529,13 @@ brorand@^1.1.0:
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
bs58@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279"
integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==
dependencies:
base-x "^4.0.0"
bs58@^4.0.0, bs58@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"