diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..9563c2ee --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/code/client/assets/wc.png b/code/client/assets/wc.png new file mode 100644 index 00000000..d94b2bea Binary files /dev/null and b/code/client/assets/wc.png differ diff --git a/code/client/package.json b/code/client/package.json index bc2b9429..e729bed5 100644 --- a/code/client/package.json +++ b/code/client/package.json @@ -24,15 +24,15 @@ "@babel/polyfill": "^7.12.1", "@babel/preset-env": "^7.13.15", "@babel/preset-react": "^7.13.13", - "@babel/runtime": "^7.14.0", - "@svgr/webpack": "^6.2.1", + "@babel/runtime": "^7.23.6", + "@svgr/webpack": "^8.1.0", "@truffle/contract": "^4.3.43", "@truffle/debug-utils": "github:polymorpher/truffle-debug-utils", "babel-loader": "^8.2.2", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "babel-preset-react": "^6.24.1", "base64-loader": "^1.0.0", - "copy-webpack-plugin": "^9.0.1", + "copy-webpack-plugin": "^11.0.0", "core-js": "^3.13.0", "css-loader": "^5.2.6", "eslint": "^7.26.0", @@ -45,21 +45,20 @@ "eslint-plugin-promise": "^5.1.0", "eslint-plugin-react": "^7.23.2", "eslint-plugin-truffle": "^0.3.1", - "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.3.1", - "less": "^4.1.1", - "less-loader": "^9.1.0", + "html-webpack-plugin": "^5.6.0", + "less": "^4.2.0", + "less-loader": "^11.1.4", "mini-css-extract-plugin": "^1.6.0", "react-player": "^2.9.0", "sass": "^1.34.1", "sass-loader": "^12.0.0", "style-loader": "^2.0.0", "tslib": "^2.3.0", - "webpack": "^5.33.2", + "webpack": "^5.89.0", "webpack-bundle-analyzer": "^3.6.1", - "webpack-cli": "4.8.0", - "webpack-dev-server": "^4.0.0", - "webpack-obfuscator": "^0.27.4" + "webpack-cli": "5.1.4", + "webpack-dev-server": "^4.15.1", + "webpack-obfuscator": "^3.5.1" }, "browserslist": { "production": [ @@ -75,6 +74,7 @@ }, "dependencies": { "@ant-design/icons": "^4.6.2", + "@babel/helper-string-parser": "^7.23.4", "@harmony-js/core": "^0.1.57", "@harmony-js/crypto": "^0.1.56", "@reduxjs/toolkit": "^1.6.0", @@ -82,6 +82,7 @@ "@sentry/react": "^6.8.0", "@sentry/tracing": "^6.8.0", "@transak/transak-sdk": "^1.0.31", + "@walletconnect/web3wallet": "^1.10.0", "antd": "4.17.0", "axios": "^0.21.1", "bn.js": "^5.2.0", @@ -116,10 +117,13 @@ "svg-country-flags": "^1.2.10", "title-case": "^3.0.3", "unique-names-generator": "^4.5.0", - "uuid": "^8.3.2", - "web3": "^1.6.1" + "uuid": "^8.3.2" }, "resolutions": { - "tar": ">=4.4.15" + "tar": ">=4.4.15", + "web3": "^4.3.0", + "web3-eth-abi": "^4.1.4", + "web3-eth-contract": "^4.1.4", + "web3-utils": "^4.1.0" } } diff --git a/code/client/src/app.less b/code/client/src/app.less index 5829a1bc..e3b1cf90 100644 --- a/code/client/src/app.less +++ b/code/client/src/app.less @@ -1,5 +1,6 @@ // https://ant.design/docs/react/customize-theme-variable // This relies on css variables: https://caniuse.com/css-variables + @import 'antd/dist/antd.variable.min.css'; @import './styles/ant.less'; // some overrides on ant styles from v1 ui diff --git a/code/client/src/components/QrCodeScanner.jsx b/code/client/src/components/QrCodeScanner.jsx index eb3d4b1c..bf702ec0 100644 --- a/code/client/src/components/QrCodeScanner.jsx +++ b/code/client/src/components/QrCodeScanner.jsx @@ -11,7 +11,7 @@ import UploadOutlined from '@ant-design/icons/UploadOutlined' import jsQR from 'jsqr' import { getDataURLFromFile, getTextFromFile } from './Common' -const QrCodeScanner = ({ onScan, shouldInit, style }) => { +const QrCodeScanner = ({ onScan, shouldInit, style, uploadBtnText = 'Use Image or JSON Instead' }) => { const ref = useRef() const { isMobile } = useWindowDimensions() const [videoDevices, setVideoDevices] = useState([]) @@ -160,7 +160,7 @@ const QrCodeScanner = ({ onScan, shouldInit, style }) => { beforeUpload={beforeUpload} onChange={onQrcodeChange} > - + diff --git a/code/client/src/components/SiderMenu.jsx b/code/client/src/components/SiderMenu.jsx index a2b91f69..821d4dc9 100644 --- a/code/client/src/components/SiderMenu.jsx +++ b/code/client/src/components/SiderMenu.jsx @@ -4,7 +4,6 @@ import Layout from 'antd/es/layout' import Image from 'antd/es/image' import Row from 'antd/es/row' import Menu from 'antd/es/menu' -import Typography from 'antd/es/typography' import PlusCircleOutlined from '@ant-design/icons/PlusCircleOutlined' import UnorderedListOutlined from '@ant-design/icons/UnorderedListOutlined' import HistoryOutlined from '@ant-design/icons/HistoryOutlined' @@ -23,24 +22,12 @@ import StakeIcon from '../assets/icons/stake.svg?el' import RestoreIcon from '../assets/icons/restore.svg?el' import config from '../config' import Paths from '../constants/paths' -import styled from 'styled-components' import util, { useWindowDimensions } from '../util' import { useDispatch, useSelector } from 'react-redux' import { useTheme, getColorPalette } from '../theme' import { StatsInfo, LineDivider } from './StatsInfo' import { globalActions } from '../state/modules/global' -const { Link } = Typography - -const SiderLink = styled(Link).attrs((e) => ({ - ...e, - style: { ...e.style }, - target: '_blank', - rel: 'noopener noreferrer' -}))` - &:hover { - opacity: 0.8; - } -` +import { SiderLink } from './Text' const mobileMenuItemStyle = { padding: '0 10px', diff --git a/code/client/src/components/Text.jsx b/code/client/src/components/Text.jsx index fcb21431..650e88bf 100644 --- a/code/client/src/components/Text.jsx +++ b/code/client/src/components/Text.jsx @@ -35,7 +35,7 @@ export const InputBox = styled(Input).attrs(({ $num, $decimal, type, autoComplet margin-bottom: ${props => props.$marginBottom || props.margin || '32px'}; border: none; border-bottom: 1px dashed black; - font-size: 16px; + font-size: ${props => props.$fontSize || '16px'}; &:hover{ border-bottom: 1px dashed black; } @@ -123,3 +123,14 @@ export const LabeledRow = ({ label, doubleRow = false, ultrawide = false, isMobi ) } + +export const SiderLink = styled(Link).attrs((e) => ({ + ...e, + style: { ...e.style }, + target: '_blank', + rel: 'noopener noreferrer' +}))` + &:hover { + opacity: 0.8; + } +` diff --git a/code/client/src/config.js b/code/client/src/config.js index f480de3a..30ae19a3 100644 --- a/code/client/src/config.js +++ b/code/client/src/config.js @@ -42,4 +42,6 @@ const config = mergeAll({}, baseConfig, { scanDelay: 250, }) +export const WalletConnectId = process.env.WALLET_CONNECT_ID || '' + export default config diff --git a/code/client/src/integration/WalletAuth.jsx b/code/client/src/integration/WalletAuth.jsx index de1b3445..6b59ed06 100644 --- a/code/client/src/integration/WalletAuth.jsx +++ b/code/client/src/integration/WalletAuth.jsx @@ -12,6 +12,7 @@ import AnimatedSection from '../components/AnimatedSection' import RequestSignature from './RequestSignature' import message from '../message' import { Text } from '../components/Text' +import WalletConnect from './WalletConnect' const WalletAuth = () => { const dispatch = useDispatch() @@ -21,8 +22,7 @@ const WalletAuth = () => { const qs = querystring.parse(location.search) const callback = qs.callback && Buffer.from(qs.callback, 'base64').toString() - const caller = qs.caller - const network = qs.network + const { wc, caller, network } = qs const { amount, dest, from, calldata } = qs const { message: msg, raw, duration, comment } = qs // if (!action || !callback || !caller) { @@ -40,6 +40,11 @@ const WalletAuth = () => { } }, [network]) + if (action === 'walletconnect') { + return + // return <> + } + if (!action || !callback || !caller) { message.error('The app did not specify a callback, an action, or its identity. Please ask the app developer to fix it.') return ( diff --git a/code/client/src/integration/WalletConnect.jsx b/code/client/src/integration/WalletConnect.jsx new file mode 100644 index 00000000..47cade5f --- /dev/null +++ b/code/client/src/integration/WalletConnect.jsx @@ -0,0 +1,348 @@ +import React, { useState, useEffect, useCallback } from 'react' +import { useSelector } from 'react-redux' +import { Hint, InputBox, SiderLink, Text } from '../components/Text' +import Image from 'antd/es/image' +import { Row } from 'antd/es/grid' +import Button from 'antd/es/button' +import AnimatedSection from '../components/AnimatedSection' +import message from '../message' +import Spin from 'antd/es/spin' +import util from '../util' +import QrCodeScanner from '../components/QrCodeScanner' +import { WalletSelector } from './Common' +import { Core } from '@walletconnect/core' +import { Web3Wallet } from '@walletconnect/web3wallet' +import { WalletConnectId } from '../config' +import api from '../api' +import { SimpleWeb3Provider } from './Web3Provider' +import WCLogo from '../../assets/wc.png' +import QrcodeOutlined from '@ant-design/icons/QrcodeOutlined' +import Space from 'antd/es/space' +import WalletAddress from '../components/WalletAddress' +import Paths from '../constants/paths' +import WalletConnectActionModal from './WalletConnectActionModal' +// see https://docs.walletconnect.com/2.0/specs/sign/error-codes +const UNSUPPORTED_CHAIN_ERROR_CODE = 5100 +const INVALID_METHOD_ERROR_CODE = 1001 +const USER_REJECTED_REQUEST_CODE = 4001 +const USER_DISCONNECTED_CODE = 6000 + +const EVMBasedNamespaces = 'eip155' + +const SupportedMethods = [ + 'eth_accounts', + 'net_version', + 'eth_chainId', + 'personal_sign', + 'eth_sign', + 'eth_signTypedData', + 'eth_signTypedData_v4', + 'eth_sendTransaction', + 'eth_blockNumber', + 'eth_getBalance', + 'eth_getCode', + 'eth_getTransactionCount', + 'eth_getStorageAt', + 'eth_getBlockByNumber', + 'eth_getBlockByHash', + 'eth_getTransactionByHash', + 'eth_getTransactionReceipt', + 'eth_estimateGas', + 'eth_call', + 'eth_getLogs', + 'eth_gasPrice', + 'wallet_getPermissions', + 'wallet_requestPermissions', +] + +const devOnlyMethods = ['eth_sign'] + +const WalletConnect = ({ wcSesssionUri }) => { + // const dispatch = useDispatch() + const wallets = useSelector(state => state.wallet) + const walletList = Object.keys(wallets).filter(addr => util.safeNormalizedAddress(addr)) + const [selectedAddress, setSelectedAddress] = useState({ value: walletList[0]?.address, label: walletList[0]?.name }) + const [loading, setLoading] = useState(false) + const [connecting, setConnecting] = useState(false) + const [isScanMode, setScanMode] = useState(false) + const [uri, setUri] = useState(wcSesssionUri || '') + const [peerMeta, setPeerMeta] = useState(null) + const [web3Wallet, setWeb3Wallet] = useState(undefined) + const [isWcInitialized, setIsWcInitialized] = useState() + const [wcSession, setWcSession] = useState() + const [error, setError] = useState('') + const [hint, setHint] = useState('') + const [web3Provider] = useState(SimpleWeb3Provider({ defaultAddress: selectedAddress.value })) + const dev = useSelector(state => state.global.dev) + + useEffect(() => { + if (!isWcInitialized && web3Wallet) { + const { chainId, activeNetwork } = api.blockchain.getChainInfo() + // we try to find a compatible active session + const activeSessions = web3Wallet.getActiveSessions() + const compatibleSession = Object.keys(activeSessions) + .map(topic => activeSessions[topic]) + .find(session => session.namespaces[EVMBasedNamespaces].accounts[0] === `${EVMBasedNamespaces}:${chainId}:${selectedAddress.value}`) + + if (compatibleSession) { + setWcSession(compatibleSession) + } + + // events + web3Wallet.on('session_proposal', async proposal => { + setConnecting(true) + const { id, params } = proposal + const { requiredNamespaces } = params + + message.debug(`Session proposal: ${JSON.stringify(proposal)}`) + + const account = `${EVMBasedNamespaces}:${chainId}:${selectedAddress.value}` + // console.log('selectedAddress', selectedAddress) + const chain = `${EVMBasedNamespaces}:${chainId}` + const events = requiredNamespaces[EVMBasedNamespaces]?.events || [] // accept all events, similar to Gnosis Safe + + try { + const wcSession = await web3Wallet.approveSession({ + id, + namespaces: { + eip155: { + accounts: [account], // only the Safe account + chains: [chain], // only the Safe chain + methods: SupportedMethods, // only the Safe methods + events, + }, + }, + }) + // console.log('wcSession', wcSession) + setWcSession(wcSession) + setError(undefined) + } catch (error) { + message.error(`WC session proposal error: ${error}`) + setError('Failed to establish WalletConnect V2 connection. Please check the console for error and contact support.') + + const errorMessage = `Connection refused: This Account is in ${activeNetwork} (chainId: ${chainId}) but the Wallet Connect session proposal is not valid because it contains: 1) A required chain different than ${activeNetwork} 2) Does not include ${activeNetwork} between the optional chains 3) No EVM compatible chain is included` + console.log(errorMessage) + await web3Wallet.rejectSession({ + id: proposal.id, + reason: { + code: UNSUPPORTED_CHAIN_ERROR_CODE, + message: errorMessage, + }, + }) + } finally { + setConnecting(false) + } + }) + + web3Wallet.on('session_delete', async () => { + setWcSession(undefined) + setError(undefined) + }) + + message.debug('WC Initialized') + + setIsWcInitialized(true) + } + }, [selectedAddress, web3Wallet, isWcInitialized]) + + useEffect(() => { + const initWalletConnect = async () => { + setLoading(true) + try { + const core = new Core({ + projectId: WalletConnectId, + }) + + const w3w = await Web3Wallet.init({ + core, + metadata: { + description: 'OTP Wallet', + url: 'https://otpwallet.xyz', + icons: ['https://1wallet.crazy.one/1wallet.png'], + name: 'OTPWallet', + } + }) + setWeb3Wallet(w3w) + } catch (error) { + message.error(`Error initializing WalletConnect: ${error}`) + setIsWcInitialized(true) + } finally { + setLoading(false) + } + } + initWalletConnect() + }, []) + + // session_request needs to be a separate Effect because a valid wcSession should be present + useEffect(() => { + if (isWcInitialized && web3Wallet && wcSession && web3Provider) { + const { chainId, activeNetwork } = api.blockchain.getChainInfo() + + web3Wallet.on('session_request', async event => { + const { topic, id } = event + const { request, chainId: transactionChainId } = event.params + const { method, params } = request + + const isWalletChainId = transactionChainId === `${EVMBasedNamespaces}:${chainId}` + + // we only accept transactions from the Safe chain + if (!isWalletChainId) { + const errorMessage = `Transaction rejected: the connected dApp is not set to the correct chain. Make sure the dApp only uses ${activeNetwork} to interact with this wallet.` + setError(errorMessage) + await web3Wallet.respondSessionRequest({ + topic, + response: rejectResponse(id, UNSUPPORTED_CHAIN_ERROR_CODE, errorMessage), + }) + return + } + + try { + setError('') + if (!dev && devOnlyMethods.includes(method)) { + throw new Error(`${method} is only available when dev mode is enabled`) + } + const result = await web3Provider.send(method, params, id, selectedAddress.value) + await web3Wallet.respondSessionRequest({ + topic, + response: { + id, + jsonrpc: '2.0', + result, + }, + }) + } catch (error) { + setError(error?.message) + const isUserRejection = error?.message?.includes?.('Transaction was rejected') + const code = isUserRejection ? USER_REJECTED_REQUEST_CODE : INVALID_METHOD_ERROR_CODE + await web3Wallet.respondSessionRequest({ + topic, + response: rejectResponse(id, code, error.message), + }) + } + }) + } + }, [ + wcSession, + isWcInitialized, + web3Wallet, + web3Provider + ]) + + useEffect(() => { + setPeerMeta(wcSession?.peer.metadata) + }, [wcSession]) + + const wcConnect = useCallback( + async (uri) => { + const isValidWalletConnectUri = uri && uri.startsWith('wc') + + try { + if (isValidWalletConnectUri && web3Wallet) { + await web3Wallet.core.pairing.pair({ uri }) + } + } catch (ex) { + message.error(`Error connecting with WalletConnect: ${ex.toString()}`) + } + }, + [web3Wallet], + ) + + const wcDisconnect = useCallback(async () => { + if (wcSession && web3Wallet) { + await web3Wallet.disconnectSession({ + topic: wcSession.topic, + reason: { + code: USER_DISCONNECTED_CODE, + message: 'User disconnected. Safe Wallet Session ended by the user', + }, + }) + setWcSession(undefined) + setError(undefined) + } + setUri('') + }, [web3Wallet, wcSession]) + + useEffect(() => { + if (!uri) { + return + } + setConnecting(true) + setTimeout(() => setHint('Tips: If the wallet is not connecting automatically, check your connecting string and try again. Refresh if necessary'), 500) + wcConnect(uri).finally(() => { + setTimeout(() => setConnecting(false), 500) + }) + }, [uri]) + + return ( + + + {loading && ( + + + )} + {error && ( + Error: {error} + )} + {/* */} + + {!wcSession && + <> + + + + e.majorVersion >= 10} showOlderVersions={false} useHex={false} /> + + Connect your wallet to any dApp (e.g. Safe, Swap, .country)

+ 1. Select the wallet you want to connect to
+ 2. Copy the connection link from the dApp's WalletConnect prompt, or scan the QR Code from that prompt
+ 3. That's it! You can now use the wallet just like MetaMask or other mobile wallets +
+ {!connecting && + + + + setUri(value)} placeholder='Scan QR Code or paste connection link here (wc:...)' /> + + {hint} + } + {connecting && } + {selectedAddress.value && isScanMode && } + } + {wcSession && ( + <> + + + + Connected + {peerMeta && {peerMeta.name} ({peerMeta.url})} + + + + Your wallet: {selectedAddress.label} + window.open(Paths.showAddress(selectedAddress.value), '_blank')} /> + + + + + + Please leave this window open, otherwise transactions will not pop up. + + )} +
+ ) +} + +export default WalletConnect + +const rejectResponse = (id, code, message) => { + return { + id, + jsonrpc: '2.0', + error: { + code, + message, + }, + } +} diff --git a/code/client/src/integration/WalletConnectActionModal.jsx b/code/client/src/integration/WalletConnectActionModal.jsx new file mode 100644 index 00000000..343a4319 --- /dev/null +++ b/code/client/src/integration/WalletConnectActionModal.jsx @@ -0,0 +1,87 @@ +import { Web3ProviderCommunicator } from './Web3Provider' +import Modal from 'antd/es/modal' +import React, { useState, useEffect } from 'react' +import Sign from '../pages/Show/Sign' +import ONEUtil from '../../../lib/util' +import Call from '../pages/Show/Call' +const WalletConnectActionModal = () => { + const [visible, setVisible] = useState(false) + const [pendingRequests, setPendingRequests] = useState([]) + const activeRequest = pendingRequests[0] + const { id, action, address, message, messageHash, typedData, tx } = activeRequest || {} + + useEffect(() => { + const sid = Web3ProviderCommunicator.subscribe((request) => { + // console.log('Received request', request) + setPendingRequests(e => [...e, request]) + }) + // console.log(`Subscribed to Web3ProviderCommunicator. id=${sid}`) + return () => { + Web3ProviderCommunicator.unsubscribe(sid) + } + }, []) + + useEffect(() => { + setVisible(!!activeRequest) + }, [activeRequest]) + const onSignSuccess = (txId, { hash, signature }) => { + const signatureStr = ONEUtil.hexString(signature) + // console.log('WC sign completed', { action, hash, message, messageHash, typedData }) + Web3ProviderCommunicator.completeRequest(id, null, { signature: signatureStr }) + } + const onCallSuccess = (txId) => { + Web3ProviderCommunicator.completeRequest(id, null, txId) + setPendingRequests(rqs => rqs.filter(e => e.id !== id)) + } + const onClose = () => { + Web3ProviderCommunicator.completeRequest(id, Error('Transaction was rejected: User cancelled')) + setPendingRequests(rqs => rqs.filter(e => e.id !== id)) + } + return ( + + {action === 'sign' && ( + )} + {action === 'signRaw' && ( + )} + {action === 'signTyped' && ( + )} + {action === 'call' && ( + )} + + ) +} + +export default WalletConnectActionModal diff --git a/code/client/src/integration/Web3Provider.jsx b/code/client/src/integration/Web3Provider.jsx new file mode 100644 index 00000000..ea708813 --- /dev/null +++ b/code/client/src/integration/Web3Provider.jsx @@ -0,0 +1,195 @@ +import api from '../api' +import util from '../util' +import { useEffect, useState } from 'react' + +const SimpleCommunicator = () => { + const requests = {} + const subscribers = {} + const enqueueRequest = (request) => { + requests[request.id] = request + for (const callback of Object.values(subscribers)) { + callback(request) + } + } + const getAllRequests = () => { + return requests + } + + const completeRequest = async (id, error, result) => { + const r = requests[id] + // console.log(`Completing ${id}`, 'error', error, 'request', r) + if (!r) { + throw new Error(`Request ${id} does not exist`) + } + await r.callback && r.callback(error, result) + delete requests[r.id] + } + + const subscribe = (callback) => { + const id = Object.keys(subscribers).length + 1 + subscribers[id] = callback + return id + } + + const unsubscribe = (id) => { + delete subscribers[id] + } + + const getRequest = (id) => { + return requests.get(id) + } + + const getTopRequest = () => { + const keys = Object.keys(requests) + if (keys.length === 0) { + return + } + let minId = keys[0] + for (const k of Object.keys(requests)) { + if (k < minId) { + minId = k + } + } + return requests[minId] + } + + return { + enqueueRequest, + completeRequest, + subscribe, + unsubscribe, + getTopRequest, + getRequest, + getAllRequests + } +} + +export const Web3ProviderCommunicator = SimpleCommunicator() +const commonSignatureCallback = (resolve, reject) => (error, result) => { + if (error) { + reject(error) + return + } + const signature = 'signature' in result ? result.signature : undefined + resolve(signature || '0x') +} +// see also https://eips.ethereum.org/EIPS/eip-1193 +export const SimpleWeb3Provider = ({ defaultAddress } = {}) => { + // For testing + // useEffect(() => { + // Web3ProviderCommunicator.enqueueRequest({ + // id: 1704163097860232, + // action: 'call', + // address: '0x3864615fA0a8bc759f4EF4c9b9e746D271b6D2Bb', + // tx: { + // gas: 251410, + // gasPrice: '0x174876e800', + // from: '0x3864615fa0a8bc759f4ef4c9b9e746d271b6d2bb', + // to: '0xc22834581ebc8527d974f8a1c97e1bea4ef910bc', + // data: '0x1688f0b9000000000000000000000000fb1bffc9d739b8d520daf37df666da4c687191ea00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000018cc7e547c90000000000000000000000000000000000000000000000000000000000000164b63e800d0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000017062a1de2fe6b99be3d9d37841fed19f57380400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000003864615fa0a8bc759f4ef4c9b9e746d271b6d2bb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + // value: '0' + // } + // }) + // }, []) + const send = async (method, params, id, address) => { + address = address ?? defaultAddress + console.log('Web3Provider send', { id, method, params, address }) + if (!address) { + throw new Error('address is not set') + } + if (method === 'eth_accounts') { + return [address] + } else if (method === 'net_version' || method === 'eth_chainId') { + const { chainId } = api.blockchain.getChainInfo() + return chainId + } else if (method === 'personal_sign') { + const [message, requestedAddress] = params + if (address.toLowerCase() !== requestedAddress?.toLowerCase()) { + throw new Error('Requested address is not wallet address') + } + return new Promise((resolve, reject) => { + const callback = commonSignatureCallback(resolve, reject) + const request = { id, action: 'sign', message, address, callback } + Web3ProviderCommunicator.enqueueRequest(request) + }) + } else if (method === 'eth_sign') { + const [requestedAddress, messageHash] = params + if (address.toLowerCase() !== requestedAddress?.toLowerCase()) { + throw new Error('Requested address is not wallet address') + } + return new Promise((resolve, reject) => { + const callback = commonSignatureCallback(resolve, reject) + const request = { id, action: 'signRaw', messageHash, address, callback } + Web3ProviderCommunicator.enqueueRequest(request) + }) + } else if (method === 'eth_signTypedData' || method === 'eth_signTypedData_v4') { + const [requestedAddress, typedData] = params + const parsedTypedData = typeof typedData === 'string' ? JSON.parse(typedData) : typedData + if (requestedAddress.toLowerCase() !== address.toLowerCase()) { + throw new Error('Requested address is not wallet address') + } + if (!util.isObjectEIP712TypedData(parsedTypedData)) { + throw new Error('Request does not conform EIP712') + } + return new Promise((resolve, reject) => { + const callback = commonSignatureCallback(resolve, reject) + const request = { id, action: 'signTyped', typedData: parsedTypedData, address, callback } + Web3ProviderCommunicator.enqueueRequest(request) + }) + } else if (method === 'eth_sendTransaction') { + const tx = { + ...params[0], + value: params[0].value || '0', + data: params[0].data || '0x', + } + + if (typeof tx.gas === 'string' && tx.gas.startsWith('0x')) { + tx.gas = parseInt(tx.gas, 16) + } + return new Promise((resolve, reject) => { + const callback = (error, result) => error ? reject(error) : resolve(result) + const request = { id, action: 'call', tx, address, callback } + Web3ProviderCommunicator.enqueueRequest(request) + }) + } else if (method === 'eth_blockNumber') { + return await api.rpc.getBlockNumber() + } else if (method === 'eth_getBalance') { + return await api.blockchain.getBalance({ address: params[0]?.toLowerCase(), blockNumber: params[1] }) + } else if (method === 'eth_getCode') { + return await api.blockchain.getCode({ address: params[0]?.toLowerCase(), blockNumber: params[1] }) + } else if (method === 'eth_getTransactionCount') { + return await api.rpc.getTransactionCount({ address: params[0]?.toLowerCase(), blockNumber: params[1] }) + } else if (method === 'eth_getStorageAt') { + return await api.rpc.getStorageAt({ address: params[0]?.toLowerCase(), position: params[1], blockNumber: params[2] }) + } else if (method === 'eth_getBlockByNumber') { + return await api.rpc.getBlockByNumber({ address: params[0]?.toLowerCase(), includeTransactionDetails: params[1] }) + } else if (method === 'eth_getBlockByHash') { + return await api.rpc.getBlockByHash({ address: params[0]?.toLowerCase(), includeTransactionDetails: params[1] }) + } else if (method === 'eth_getTransactionByHash') { + return await api.rpc.getTransaction(params[0]) + } else if (method === 'eth_getTransactionReceipt') { + const hash = params[0] + return await api.rpc.getTransactionReceipt(hash) + } else if (method === 'eth_estimateGas') { + return await api.rpc.getEstimateGas({ transaction: params[0] }) + } else if (method === 'eth_call') { + return await api.rpc.simulateCall({ transaction: params[0] }) + } else if (method === 'eth_getLogs') { + return await api.rpc.getLogs({ filter: params[0] }) + } else if (method === 'eth_gasPrice') { + return await api.rpc.gasPrice() + } else if (method === 'wallet_getPermissions') { + // TODO: https://eips.ethereum.org/EIPS/eip-2255 + return [] + } else if (method === 'wallet_requestPermissions') { + // TODO: https://eips.ethereum.org/EIPS/eip-2255 + return [] + } else { + throw Error(`${method} is not implemented`) + } + } + + return { + send + } +} diff --git a/code/client/src/pages/Show/Call.jsx b/code/client/src/pages/Show/Call.jsx index 58f90c3c..f8d69386 100644 --- a/code/client/src/pages/Show/Call.jsx +++ b/code/client/src/pages/Show/Call.jsx @@ -148,6 +148,7 @@ const Call = ({ currentWallet={wallet} disabled={!!prefillDest} /> + (equivalent to {util.safeNormalizedAddress(transferTo.value)}) diff --git a/code/client/src/pages/Show/Sign.jsx b/code/client/src/pages/Show/Sign.jsx index a826ab5b..d91506ce 100644 --- a/code/client/src/pages/Show/Sign.jsx +++ b/code/client/src/pages/Show/Sign.jsx @@ -32,6 +32,8 @@ const Sign = ({ prefillMessageInput, // optional string, the message itself prefillUseRawMessage, // optional boolean, whether or not eth signing header should be attached. True means not to attach header prefillDuration, // optional string that can be parsed into an integer, the number of milliseconds of the validity of the signature + prefillEip712TypedData, + skipHash, shouldAutoFocus, headless, }) => { @@ -43,8 +45,8 @@ const Sign = ({ const doubleOtp = wallet.doubleOtp const { otpInput, otp2Input, resetOtp } = otpState - const [messageInput, setMessageInput] = useState(prefillMessageInput) - const [useRawMessage, setUseRawMessage] = useState(prefillUseRawMessage) + const [messageInput, setMessageInput] = useState(prefillEip712TypedData ? JSON.stringify(prefillEip712TypedData, null, 2) : prefillMessageInput) + const [useRawMessage, setUseRawMessage] = useState(prefillUseRawMessage || prefillEip712TypedData) prefillDuration = parseInt(prefillDuration) const [duration, setDuration] = useState(prefillDuration) @@ -63,10 +65,12 @@ const Sign = ({ if (invalidOtp || invalidOtp2) return let message = messageInput - if (!useRawMessage) { + if (prefillEip712TypedData) { + message = ONEUtil.hexStringToBytes(ONEUtil.encodeEIP712TypedData(prefillEip712TypedData)) + } else if (!useRawMessage) { message = ONEUtil.ethMessage(message) } - const hash = ONEUtil.keccak(message) + const hash = !skipHash ? ONEUtil.keccak(message) : message const tokenId = new BN(hash).toString() const expiryAt = noExpiry ? 0xffffffff : Math.floor(((Date.now() + duration) / 1000)) @@ -113,13 +117,21 @@ const Sign = ({ const inner = ( <> + {prefillEip712TypedData && ( + + Note: This request is strongly typed (EIP712) so to allow more readability + + )}