Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🦊 Metamask integration POC #4835

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/ui/.env.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# TESTNET Endpoints
REACT_APP_TESTNET_NODE_SOCKET=wss://rpc.joystream.org:9944
REACT_APP_TESTNET_NODE_HTTP_RPC=https://rpc.joystream.org
REACT_APP_TESTNET_QUERY_NODE=https://query.joystream.org/graphql
REACT_APP_TESTNET_QUERY_NODE_SOCKET=wss://query.joystream.org/graphql
REACT_APP_TESTNET_MEMBERSHIP_FAUCET_URL=https://faucet.joystream.org/member-faucet/register
REACT_APP_TESTNET_BACKEND=http://localhost:3000

# MAINNET Endpoints
REACT_APP_MAINNET_NODE_SOCKET=wss://rpc.joystream.org:9944
REACT_APP_MAINNET_NODE_HTTP_RPC=https://rpc.joystream.org
REACT_APP_MAINNET_QUERY_NODE=https://query.joystream.org/graphql
REACT_APP_MAINNET_QUERY_NODE_SOCKET=wss://query.joystream.org/graphql
REACT_APP_MAINNET_MEMBERSHIP_FAUCET_URL=https://faucet.joystream.org/member-faucet/register
Expand All @@ -21,6 +23,9 @@ REACT_APP_AVATAR_UPLOAD_URL=https://atlas-services.joystream.org/avatars
# WalletConnect project id
REACT_APP_WALLET_CONNECT_PROJECT_ID="2ea3f3ghubh32b8ie2f2"

# Metamask snap id (`local:http://localhost:8081` for local development)
REACT_APP_METAMASK_SNAP_ID="npm:@chainsafe/polkadot-snap"

# Image reporting

## Manual blacklist:
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
},
"dependencies": {
"@apollo/client": "3.5.7",
"@chainsafe/metamask-polkadot-adapter": "^0.6.0",
"@hcaptcha/react-hcaptcha": "^1.4.4",
"@joystream/js": "1.10.0",
"@joystream/markdown-editor": "^0.1.0",
Expand Down Expand Up @@ -97,6 +98,7 @@
"@babel/preset-env": "7",
"@babel/preset-react": "7",
"@babel/preset-typescript": "7",
"@chainsafe/metamask-polkadot-types": "^0.6.0",
"@graphql-codegen/cli": "^2.2.0",
"@graphql-codegen/near-operation-file-preset": "^2.1.4",
"@graphql-codegen/typescript": "^2.2.2",
Expand Down
83 changes: 83 additions & 0 deletions packages/ui/src/accounts/model/metamask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { enablePolkadotSnap } from '@chainsafe/metamask-polkadot-adapter'
import { MetamaskSnapApi } from '@chainsafe/metamask-polkadot-adapter/build/types'
import { SnapNetworks } from '@chainsafe/metamask-polkadot-types'
import { Signer } from '@polkadot/types/types'
import { BaseDotsamaWallet, SubscriptionFn, WalletAccount } from 'injectweb3-connect'

import MetamaskLogo from '@/app/assets/images/logos/Metamask.svg'
import { CHAIN_PROPERTIES } from '@/app/constants/chain'

const networkName = 'joystream' as SnapNetworks
const addressPrefix = CHAIN_PROPERTIES.ss58Format
const unit = { symbol: CHAIN_PROPERTIES.tokenSymbol[0], decimals: CHAIN_PROPERTIES.tokenDecimals[0] }

export class Metamask extends BaseDotsamaWallet {
protected _snapId: string
protected _httpRpcUrl: string
protected _snapApi: MetamaskSnapApi | undefined
protected _accounts: WalletAccount[] | undefined
protected _txId = 0

constructor(snapId: string, httpRpcUrl: string) {
super({
extensionName: 'Metamask',
title: 'Metamask',
logo: { src: MetamaskLogo, alt: 'Metamask Logo' },
})

this._snapId = snapId
this._httpRpcUrl = httpRpcUrl
}

public enable = async (): Promise<void> => {
const snap = await enablePolkadotSnap(
{ networkName, wsRpcUrl: this._httpRpcUrl, addressPrefix, unit },
this._snapId
)

this._snapApi = await snap.getMetamaskSnapApi()
const address = await this._snapApi.getAddress()
this._accounts = [
{
name: 'Metamask account',
address,
source: this.extensionName,
},
]

this._snapApi.signPayloadJSON
}

public getAccounts = async (): Promise<WalletAccount[]> => {
return this._accounts ?? []
}

public subscribeAccounts: (callback: SubscriptionFn) => Promise<() => void> = (callback) => {
callback(this._accounts ?? [])
return Promise.resolve(() => undefined)
}

public get signer(): Signer {
return {
signPayload: async (payload) => {
if (!this._snapApi) {
throw Error('Metamask was accessed before it was enabled')
}

const signature = (await this._snapApi.signPayloadJSON(payload)) as `0x${string}`

return { id: this._txId++, signature }
},

signRaw: async (raw) => {
if (!this._snapApi) {
throw Error('Metamask was accessed before it was enabled')
}

const signature = (await this._snapApi.signPayloadRaw(raw)) as `0x${string}`

return { id: this._txId++, signature }
},
}
}
}
55 changes: 41 additions & 14 deletions packages/ui/src/accounts/providers/accounts/useWallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { groupBy } from 'lodash'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Subject, firstValueFrom } from 'rxjs'

import { Metamask } from '@/accounts/model/metamask'
import { WalletConnect } from '@/accounts/model/walletConnect'
import { RecommendedWallets, RecommendedWalletsNames, asWallet } from '@/accounts/model/wallets'
import { useApi } from '@/api/hooks/useApi'
import { useLocalStorage } from '@/common/hooks/useLocalStorage'
import { useNetworkEndpoints } from '@/common/hooks/useNetworkEndpoints'

type WalletState = undefined | 'ENABLING' | 'READY' | 'APP_REJECTED'

Expand All @@ -21,6 +23,27 @@ const genesisHash$ = new Subject<string>()
const WalletDisconnection$ = new Subject<void>()

export const useWallets = (): UseWallets => {
const walletExtensions = useWalletExtensions()
const walletConnect = useWalletConnect(() => setWallet(undefined))
const metamask = useMetamask()

const allWallets: Wallet[] = useMemo(
() => [
...walletExtensions.installed,
...walletExtensions.unknown,
...metamask,
...walletConnect,
...walletExtensions.recommended,
],
[walletExtensions, walletConnect]
)

const { wallet, setWallet, walletState } = useSelectedWallet(allWallets)

return { allWallets, wallet, setWallet, walletState }
}

const useWalletExtensions = (): { installed: Wallet[]; recommended: Wallet[]; unknown: Wallet[] } => {
const [installedWalletsNames, setInstalledWalletsNames] = useState<string[]>([])

useEffect(() => {
Expand All @@ -43,40 +66,44 @@ export const useWallets = (): UseWallets => {
return () => clearInterval(intervalId)
}, [])

const walletExtensions = useMemo(() => {
return useMemo(() => {
const unknown = installedWalletsNames.filter((name) => !RecommendedWalletsNames.includes(name)).map(asWallet)
const { installed = [], recommended = [] } = groupBy(RecommendedWallets, (wallet) =>
installedWalletsNames.includes(wallet.extensionName) ? 'installed' : 'recommended'
)
return { installed, recommended, unknown }
}, [installedWalletsNames])
}

const useWalletConnect = (disconnect: () => void): WalletConnect[] => {
const { api } = useApi()
useEffect(() => {
if (api) genesisHash$.next(api.genesisHash.toHex())
}, [api?.isConnected])

const walletConnect = useMemo(() => {
const walletConnect: WalletConnect | undefined = useMemo(() => {
const wcProjectId: string | undefined = process.env.REACT_APP_WALLET_CONNECT_PROJECT_ID
if (!wcProjectId) return

const genesisHash = firstValueFrom(genesisHash$)
return new WalletConnect(wcProjectId, genesisHash, WalletDisconnection$, () => setWallet(undefined))
return new WalletConnect(wcProjectId, genesisHash, WalletDisconnection$, disconnect)
}, [])

const allWallets = useMemo(
() => [
...walletExtensions.installed,
...walletExtensions.unknown,
...(walletConnect ? [walletConnect] : []),
...walletExtensions.recommended,
],
[walletExtensions, walletConnect]
)
return useMemo(() => (walletConnect ? [walletConnect] : []), [walletConnect])
}

const { wallet, setWallet, walletState } = useSelectedWallet(allWallets)
const useMetamask = (): Metamask[] => {
const [endpoints] = useNetworkEndpoints()

return { allWallets, wallet, setWallet, walletState }
const metamask: Metamask | undefined = useMemo(() => {
const snapId = process.env.REACT_APP_METAMASK_SNAP_ID
const isMetaMask = (window as any)?.ethereum?.isMetaMask
if (!snapId || !isMetaMask) return

return new Metamask(snapId, endpoints.nodeHttpRpcEndpoint)
}, [endpoints.nodeHttpRpcEndpoint])

return useMemo(() => (metamask ? [metamask] : []), [metamask])
}

const useSelectedWallet = (allWallets: Wallet[]) => {
Expand Down
43 changes: 43 additions & 0 deletions packages/ui/src/app/assets/images/logos/Metamask.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions packages/ui/src/app/config/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type NetworkType = 'mainnet' | 'local' | 'testnet' | 'auto-conf' | 'local

export interface NetworkEndpoints {
nodeRpcEndpoint: string
nodeHttpRpcEndpoint: string
queryNodeEndpoint: string
queryNodeEndpointSubscription: string
membershipFaucetEndpoint: string
Expand All @@ -10,6 +11,7 @@ export interface NetworkEndpoints {
}

const TESTNET_NODE_SOCKET = process.env.REACT_APP_TESTNET_NODE_SOCKET
const TESTNET_NODE_HTTP_RPC = process.env.REACT_APP_TESTNET_NODE_HTTP_RPC
const TESTNET_QUERY_NODE = process.env.REACT_APP_TESTNET_QUERY_NODE
const TESTNET_QUERY_NODE_SOCKET = process.env.REACT_APP_TESTNET_QUERY_NODE_SOCKET
const TESTNET_MEMBERSHIP_FAUCET_URL = process.env.REACT_APP_TESTNET_MEMBERSHIP_FAUCET_URL
Expand All @@ -19,6 +21,7 @@ export const IS_TESTNET_DEFINED =
TESTNET_NODE_SOCKET && TESTNET_QUERY_NODE && TESTNET_QUERY_NODE_SOCKET && TESTNET_MEMBERSHIP_FAUCET_URL

const MAINNET_NODE_SOCKET = process.env.REACT_APP_MAINNET_NODE_SOCKET
const MAINNET_NODE_HTTP_RPC = process.env.REACT_APP_MAINNET_NODE_HTTP_RPC
const MAINNET_QUERY_NODE = process.env.REACT_APP_MAINNET_QUERY_NODE
const MAINNET_QUERY_NODE_SOCKET = process.env.REACT_APP_MAINNET_QUERY_NODE_SOCKET
const MAINNET_MEMBERSHIP_FAUCET_URL = process.env.REACT_APP_MAINNET_MEMBERSHIP_FAUCET_URL
Expand Down Expand Up @@ -57,6 +60,13 @@ const NODE_RPC_ENDPOINT: PredefinedEndpoint = {
'local-mocks': 'ws://127.0.0.1:9944',
}

const NODE_HTTP_RPC_ENDPOINT: PredefinedEndpoint = {
mainnet: MAINNET_NODE_HTTP_RPC,
local: 'http://127.0.0.1:9933', // TODO: check
testnet: TESTNET_NODE_HTTP_RPC,
'local-mocks': 'http://127.0.0.1:9933', // TODO: check
}

const BACKEND_ENDPOINT: PredefinedEndpoint = {
mainnet: MAINNET_BACKEND,
local: 'http://localhost:3000',
Expand All @@ -66,6 +76,7 @@ const BACKEND_ENDPOINT: PredefinedEndpoint = {

export const pickEndpoints = (network: NetworkType): Partial<NetworkEndpoints> => ({
nodeRpcEndpoint: NODE_RPC_ENDPOINT[network],
nodeHttpRpcEndpoint: NODE_HTTP_RPC_ENDPOINT[network],
queryNodeEndpoint: QUERY_NODE_ENDPOINT[network],
queryNodeEndpointSubscription: QUERY_NODE_ENDPOINT_SUBSCRIPTION[network],
membershipFaucetEndpoint: MEMBERSHIP_FAUCET_ENDPOINT[network],
Expand Down