From 589a76652a5456cf8c06ecca2c49c6305e3fc1de Mon Sep 17 00:00:00 2001 From: yann300 Date: Mon, 30 Sep 2024 17:39:09 +0200 Subject: [PATCH] EIP 712 --- .../src/app/tabs/locales/en/filePanel.json | 4 +- .../src/app/tabs/locales/en/udapp.json | 7 +- apps/remix-ide/src/blockchain/providers/vm.ts | 22 ++++- .../run-tab/src/lib/actions/account.ts | 5 + .../remix-ui/run-tab/src/lib/actions/index.ts | 3 +- .../run-tab/src/lib/components/account.tsx | 94 ++++++++----------- .../run-tab/src/lib/components/settingsUI.tsx | 1 + libs/remix-ui/run-tab/src/lib/run-tab.tsx | 4 +- libs/remix-ui/run-tab/src/lib/types/index.ts | 2 + .../src/lib/actions/{index.ts => index.tsx} | 30 +++++- .../components/file-explorer-context-menu.tsx | 7 +- .../workspace/src/lib/contexts/index.ts | 1 + .../src/lib/providers/FileSystemProvider.tsx | 6 ++ .../workspace/src/lib/remix-ui-workspace.tsx | 11 +++ .../remix-ui/workspace/src/lib/types/index.ts | 2 + .../remix-ui/workspace/src/lib/utils/index.ts | 7 ++ 16 files changed, 141 insertions(+), 65 deletions(-) rename libs/remix-ui/workspace/src/lib/actions/{index.ts => index.tsx} (97%) diff --git a/apps/remix-ide/src/app/tabs/locales/en/filePanel.json b/apps/remix-ide/src/app/tabs/locales/en/filePanel.json index 6b02520e94d..a70e35c278d 100644 --- a/apps/remix-ide/src/app/tabs/locales/en/filePanel.json +++ b/apps/remix-ide/src/app/tabs/locales/en/filePanel.json @@ -148,5 +148,7 @@ "filePanel.saveCodeSample": "This code-sample workspace will not be persisted. Click here to save it.", "filePanel.logInGithub": "Sign in to GitHub.", "filePanel.gitHubLoggedAs": "Signed in as {githubuser}", - "filePanel.updateSubmodules": "Update all submodules of repository. Click to pull dependencies." + "filePanel.updateSubmodules": "Update all submodules of repository. Click to pull dependencies.", + "filePanel.signTypedData": "Sign Typed Data", + "filePanel.signTypedDataError": "Error while signing this typed data." } diff --git a/apps/remix-ide/src/app/tabs/locales/en/udapp.json b/apps/remix-ide/src/app/tabs/locales/en/udapp.json index fad3d8344b3..846d96c9261 100644 --- a/apps/remix-ide/src/app/tabs/locales/en/udapp.json +++ b/apps/remix-ide/src/app/tabs/locales/en/udapp.json @@ -161,5 +161,10 @@ "udapp.ganacheProviderText1": "Note: To run Ganache on your system, run:", "udapp.ganacheProviderText2": "For more info, visit: Ganache Documentation", "udapp.hardhatProviderText1": "Note: To run Hardhat network node on your system, go to hardhat project folder and run command:", - "udapp.hardhatProviderText2": "For more info, visit: Hardhat Documentation" + "udapp.hardhatProviderText2": "For more info, visit: Hardhat Documentation", + "udapp.EIP712-1": "Signing message now only supports EIP-712.", + "udapp.EIP712-2": "Please follow this link to get more information.", + "udapp.EIP712-3": "In Remix, signing typed data is possible by right clicking (right click / Sign Typed Data) on a JSON file whose content is EIP-712 compatible.", + "udapp.EIP712-create-template": "Create a JSON compliant with EIP-712", + "udapp.EIP712-close": "Close" } diff --git a/apps/remix-ide/src/blockchain/providers/vm.ts b/apps/remix-ide/src/blockchain/providers/vm.ts index 81f74137f9f..21d087cb968 100644 --- a/apps/remix-ide/src/blockchain/providers/vm.ts +++ b/apps/remix-ide/src/blockchain/providers/vm.ts @@ -1,4 +1,4 @@ -import { Web3, FMT_BYTES, FMT_NUMBER, LegacySendAsyncProvider } from 'web3' +import { Web3, FMT_BYTES, FMT_NUMBER, LegacySendAsyncProvider, LegacyRequestProvider } from 'web3' import { fromWei, toBigInt } from 'web3-utils' import { privateToAddress, hashPersonalMessage, isHexString, bytesToHex } from '@ethereumjs/util' import { extend, JSONRPCRequestPayload, JSONRPCResponseCallback } from '@remix-project/remix-simulator' @@ -10,6 +10,7 @@ export class VMProvider { worker: Worker provider: { sendAsync: (query: JSONRPCRequestPayload, callback: JSONRPCResponseCallback) => void + request: (query: JSONRPCRequestPayload) => Promise } newAccountCallback: {[stamp: number]: (error: Error, address: string) => void} constructor (executionContext: ExecutionContext) { @@ -38,14 +39,17 @@ export class VMProvider { return new Promise((resolve, reject) => { this.worker.addEventListener('message', (msg) => { if (msg.data.cmd === 'sendAsyncResult' && stamps[msg.data.stamp]) { + let result = msg.data.result + if (stamps[msg.data.stamp].request && msg.data.result) result = msg.data.result.result + if (stamps[msg.data.stamp].callback) { - stamps[msg.data.stamp].callback(msg.data.error, msg.data.result) + stamps[msg.data.stamp].callback(msg.data.error, result) return } if (msg.data.error) { stamps[msg.data.stamp].reject(msg.data.error) } else { - stamps[msg.data.stamp].resolve(msg.data.result) + stamps[msg.data.stamp].resolve(result) } } else if (msg.data.cmd === 'initiateResult') { if (!msg.data.error) { @@ -54,12 +58,20 @@ export class VMProvider { return new Promise((resolve, reject) => { const stamp = Date.now() + incr incr++ - stamps[stamp] = { callback, resolve, reject } + stamps[stamp] = { callback, resolve, reject, sendAsync: true } + this.worker.postMessage({ cmd: 'sendAsync', query, stamp }) + }) + }, + request: (query) => { + return new Promise((resolve, reject) => { + const stamp = Date.now() + incr + incr++ + stamps[stamp] = { resolve, reject, request: true } this.worker.postMessage({ cmd: 'sendAsync', query, stamp }) }) } } - this.web3 = new Web3(this.provider as LegacySendAsyncProvider) + this.web3 = new Web3(this.provider as (LegacySendAsyncProvider | LegacyRequestProvider)) this.web3.setConfig({ defaultTransactionType: '0x0' }) extend(this.web3) this.executionContext.setWeb3(this.executionContext.getProvider(), this.web3) diff --git a/libs/remix-ui/run-tab/src/lib/actions/account.ts b/libs/remix-ui/run-tab/src/lib/actions/account.ts index 7224f331611..4d937cdc5eb 100644 --- a/libs/remix-ui/run-tab/src/lib/actions/account.ts +++ b/libs/remix-ui/run-tab/src/lib/actions/account.ts @@ -95,3 +95,8 @@ export const signMessageWithAddress = (plugin: RunTab, dispatch: React.Dispatch< dispatch(displayNotification('Signed Message', modalContent(msgHash, signedData), 'OK', null, () => {}, null)) }) } + +export const addFileInternal = async (plugin: RunTab, path: string, content: string) => { + const file = await plugin.call('fileManager', 'writeFileNoRewrite', path, content) + await plugin.call('fileManager', 'open', file.newPath) +} diff --git a/libs/remix-ui/run-tab/src/lib/actions/index.ts b/libs/remix-ui/run-tab/src/lib/actions/index.ts index f40928a9919..76b418a97a2 100644 --- a/libs/remix-ui/run-tab/src/lib/actions/index.ts +++ b/libs/remix-ui/run-tab/src/lib/actions/index.ts @@ -2,7 +2,7 @@ import React from 'react' import { RunTab } from '../types/run-tab' import { resetAndInit, setupEvents, setEventsDispatch } from './events' -import { createNewBlockchainAccount, setExecutionContext, signMessageWithAddress } from './account' +import { createNewBlockchainAccount, setExecutionContext, signMessageWithAddress, addFileInternal } from './account' import { clearInstances, clearPopUp, removeInstance, pinInstance, unpinInstance, setAccount, setGasFee, setMatchPassphrasePrompt, setNetworkNameFromProvider, setPassphrasePrompt, setSelectedContract, setSendTransactionValue, setUnit, updateBaseFeePerGas, updateConfirmSettings, updateGasPrice, updateGasPriceStatus, updateMaxFee, updateMaxPriorityFee, updateScenarioPath } from './actions' @@ -32,6 +32,7 @@ export const initRunTab = (udapp: RunTab, resetEventsAndAccounts: boolean) => as } } +export const addFile = (path: string, content: string) => addFileInternal(plugin, path, content) export const setAccountAddress = (account: string) => setAccount(dispatch, account) export const setUnitValue = (unit: 'ether' | 'finney' | 'gwei' | 'wei') => setUnit(dispatch, unit) export const setGasFeeAmount = (value: number) => setGasFee(dispatch, value) diff --git a/libs/remix-ui/run-tab/src/lib/components/account.tsx b/libs/remix-ui/run-tab/src/lib/components/account.tsx index d7097a1ab0f..bba5c622114 100644 --- a/libs/remix-ui/run-tab/src/lib/components/account.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/account.tsx @@ -100,41 +100,22 @@ export function AccountUI(props: AccountProps) { return props.tooltip(intl.formatMessage({ id: 'udapp.tooltipText1' })) } - if (selectExEnv === 'web3') { - return props.modal( - intl.formatMessage({ id: 'udapp.modalTitle1' }), - , - intl.formatMessage({ id: 'udapp.ok' }), - () => { - props.modal( - intl.formatMessage({ id: 'udapp.signAMessage' }), - signMessagePrompt(), - intl.formatMessage({ id: 'udapp.ok' }), - () => { - props.signMessageWithAddress(selectedAccount, messageRef.current, signedMessagePrompt, props.passphrase) - props.setPassphrase('') - }, - intl.formatMessage({ id: 'udapp.cancel' }), - null - ) - }, - intl.formatMessage({ id: 'udapp.cancel' }), - () => { - props.setPassphrase('') - } - ) - } - props.modal( - intl.formatMessage({ id: 'udapp.signAMessage' }), - signMessagePrompt(), - intl.formatMessage({ id: 'udapp.ok' }), - () => { - props.signMessageWithAddress(selectedAccount, messageRef.current, signedMessagePrompt) - }, - intl.formatMessage({ id: 'udapp.cancel' }), - null - ) + 'Message signing', +
+
{intl.formatMessage({ id: 'udapp.EIP712-1' })}
+
{intl.formatMessage({ id: 'udapp.EIP712-2' }, { + a: (chunks) => ( + + {chunks} + + ) + })}
+
{intl.formatMessage({ id: 'udapp.EIP712-3' })}
, + intl.formatMessage({ id: 'udapp.EIP712-create-template' }), + () => { props.addFile('EIP-712-data.json', JSON.stringify(EIP712_Example, null, '\t')) }, + intl.formatMessage({ id: 'udapp.EIP712-close' }), + () => {}) } const handlePassphrase = (e) => { @@ -177,25 +158,6 @@ export function AccountUI(props: AccountProps) { ) } - const signedMessagePrompt = (msgHash: string, signedData: string) => { - return ( -
- - - {msgHash} - - - - {signedData} - -
- ) - } - return (
) } + +const EIP712_Example = { + domain: { + chainId: 1, + name: "Example App", + verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + version: "1", + }, + message: { + prompt: "Welcome! In order to authenticate to this website, sign this request and your public address will be sent to the server in a verifiable way.", + createdAt: 1718570375196, + }, + primaryType: 'AuthRequest', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + AuthRequest: [ + { name: 'prompt', type: 'string' }, + { name: 'createdAt', type: 'uint256' }, + ], + }, +} \ No newline at end of file diff --git a/libs/remix-ui/run-tab/src/lib/components/settingsUI.tsx b/libs/remix-ui/run-tab/src/lib/components/settingsUI.tsx index d3d8ed59577..f9e09246f90 100644 --- a/libs/remix-ui/run-tab/src/lib/components/settingsUI.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/settingsUI.tsx @@ -15,6 +15,7 @@ export function SettingsUI(props: SettingsProps) {
void, setExecutionContext: (executionContext: { context: string, fork: string }) => void, createNewBlockchainAccount: (cbMessage: JSX.Element) => void, setPassphrase: (passphrase: string) => void, @@ -180,6 +181,7 @@ export interface AccountProps { isSuccessful: boolean, error: string }, + addFile: (path: string, content: string) => void, setAccount: (account: string) => void, personalMode: boolean, createNewBlockchainAccount: (cbMessage: JSX.Element) => void, diff --git a/libs/remix-ui/workspace/src/lib/actions/index.ts b/libs/remix-ui/workspace/src/lib/actions/index.tsx similarity index 97% rename from libs/remix-ui/workspace/src/lib/actions/index.ts rename to libs/remix-ui/workspace/src/lib/actions/index.tsx index e10fa20d43d..fcdc24e3d34 100644 --- a/libs/remix-ui/workspace/src/lib/actions/index.ts +++ b/libs/remix-ui/workspace/src/lib/actions/index.tsx @@ -10,8 +10,7 @@ import { fetchContractFromEtherscan, fetchContractFromBlockscout } from '@remix- import JSZip from 'jszip' import { Actions, FileTree } from '../types' import IpfsHttpClient from 'ipfs-http-client' -import { AppModal } from '@remix-ui/app' -import { MessageWrapper } from '../components/file-explorer' +import { AppModal, ModalTypes } from '@remix-ui/app' export * from './events' export * from './workspace' @@ -510,6 +509,33 @@ export const runScript = async (path: string) => { }) } +export const signTypedData = async (path: string) => { + const typedData = await plugin.call('fileManager', 'readFile', path) + const web3 = await plugin.call('blockchain', 'web3') + const accounts = await web3.eth.getAccounts() + + let parsed + try { + parsed = JSON.parse(typedData) + } catch (err) { + dispatch(displayPopUp(`${path} isn't a valid JSON.`)) + return + } + + try { + const result = await web3.currentProvider.request({ + method: 'eth_signTypedData', + params: [accounts[0], parsed] + }) + + plugin.call('terminal', 'log', { type: 'log', value: `${path} signature : ${result}` }) + } catch (e) { + console.error(e) + plugin.call('terminal', 'log', { type: 'error', value: `error while signing ${path}: ${e}` }) + dispatch(displayPopUp(e.message)) + } +} + export const emitContextMenuEvent = async (cmd: customAction) => { await plugin.call(cmd.id, cmd.name, cmd) } diff --git a/libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx b/libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx index b3a39f974cd..0f408066aa2 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx @@ -41,6 +41,7 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => downloadPath, uploadFile, publishManyFilesToGist, + signTypedData, ...otherProps } = props const contextMenuRef = useRef(null) @@ -233,7 +234,11 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => case 'Publish Workspace to Gist': _paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'publishWorkspace']) publishFolderToGist(path) - break + break + case 'Sign Typed Data': + _paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'signTypedData']) + signTypedData(path) + break default: _paq.push(['trackEvent', 'fileExplorer', 'contextMenu', `${item.id}/${item.name}`]) emit && emit({ ...item, path: [path]} as customAction) diff --git a/libs/remix-ui/workspace/src/lib/contexts/index.ts b/libs/remix-ui/workspace/src/lib/contexts/index.ts index 47fba81fedb..f13b39f820b 100644 --- a/libs/remix-ui/workspace/src/lib/contexts/index.ts +++ b/libs/remix-ui/workspace/src/lib/contexts/index.ts @@ -32,6 +32,7 @@ export const FileSystemContext = createContext<{ dispatchCopyShareURL: (path: string) => Promise, dispatchCopyFolder: (src: string, dest: string) => Promise, dispatchRunScript: (path: string) => Promise, + dispatchSignTypedData: (path: string) => Promise, dispatchEmitContextMenuEvent: (cmd: customAction) => Promise, dispatchHandleClickFile: (path: string, type: 'file' | 'folder' ) => Promise dispatchHandleExpandPath: (paths: string[]) => Promise, diff --git a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx index 12c5dc516f6..3e923c7b764 100644 --- a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx +++ b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx @@ -25,6 +25,7 @@ import { copyShareURL, copyFolder, runScript, + signTypedData, emitContextMenuEvent, handleClickFile, handleExpandPath, @@ -171,6 +172,10 @@ export const FileSystemProvider = (props: WorkspaceProps) => { await runScript(path) } + const dispatchSignTypedData = async (path: string) => { + await signTypedData(path) + } + const dispatchEmitContextMenuEvent = async (cmd: customAction) => { await emitContextMenuEvent(cmd) } @@ -358,6 +363,7 @@ export const FileSystemProvider = (props: WorkspaceProps) => { dispatchCopyShareURL, dispatchCopyFolder, dispatchRunScript, + dispatchSignTypedData, dispatchEmitContextMenuEvent, dispatchHandleClickFile, dispatchHandleExpandPath, diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx index 3673851d110..0bd53dd779c 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -706,6 +706,14 @@ export function Workspace() { } } + const signTypedData = async (path: string) => { + try { + global.dispatchSignTypedData(path) + } catch (error) { + global.toast(intl.formatMessage({ id: 'filePanel.signTypedDataError' })) + } + } + const emitContextMenuEvent = (cmd: customAction) => { try { global.dispatchEmitContextMenuEvent(cmd) @@ -1135,6 +1143,7 @@ export function Workspace() { dispatchCopyFolder={global.dispatchCopyFolder} dispatchPublishToGist={global.dispatchPublishToGist} dispatchRunScript={global.dispatchRunScript} + dispatchSignTypedData={global.dispatchSignTypedData} dispatchEmitContextMenuEvent={global.dispatchEmitContextMenuEvent} dispatchHandleClickFile={global.dispatchHandleClickFile} dispatchSetFocusElement={global.dispatchSetFocusElement} @@ -1211,6 +1220,7 @@ export function Workspace() { dispatchCopyFolder={global.dispatchCopyFolder} dispatchPublishToGist={global.dispatchPublishToGist} dispatchRunScript={global.dispatchRunScript} + dispatchSignTypedData={global.dispatchSignTypedData} // dispatchEmitContextMenuEvent={global.dispatchEmitContextMenuEvent} dispatchHandleClickFile={global.dispatchHandleClickFile} dispatchSetFocusElement={global.dispatchSetFocusElement} @@ -1385,6 +1395,7 @@ export function Workspace() { deletePath={deletePath} renamePath={editModeOn} runScript={runScript} + signTypedData={signTypedData} copy={handleCopyClick} paste={handlePasteClick} copyFileName={handleCopyFileNameClick} diff --git a/libs/remix-ui/workspace/src/lib/types/index.ts b/libs/remix-ui/workspace/src/lib/types/index.ts index 214c1196b24..cd42ee7cb46 100644 --- a/libs/remix-ui/workspace/src/lib/types/index.ts +++ b/libs/remix-ui/workspace/src/lib/types/index.ts @@ -128,6 +128,7 @@ export interface FileExplorerProps { dispatchCopyShareURL: (path:string) => Promise, dispatchCopyFolder: (src: string, dest: string) => Promise, dispatchRunScript: (path: string) => Promise, + dispatchSignTypedData: (path: string) => Promise, dispatchPublishToGist: (path?: string, type?: string) => Promise, dispatchEmitContextMenuEvent: (cmd: customAction) => Promise, dispatchHandleClickFile: (path: string, type: WorkspaceElement) => Promise, @@ -194,6 +195,7 @@ export interface FileExplorerContextMenuProps { pushChangesToGist?: (path?: string) => void publishFolderToGist?: (path?: string) => void publishFileToGist?: (path?: string) => void + signTypedData?: (path?: string) => void runScript?: (path: string) => void emit?: (cmd: customAction) => void pageX: number diff --git a/libs/remix-ui/workspace/src/lib/utils/index.ts b/libs/remix-ui/workspace/src/lib/utils/index.ts index a0dfc8df6a8..033c50bb55c 100644 --- a/libs/remix-ui/workspace/src/lib/utils/index.ts +++ b/libs/remix-ui/workspace/src/lib/utils/index.ts @@ -80,6 +80,13 @@ export const contextMenuActions: MenuItems = [{ multiselect: false, label: '', group: 3 +}, { + id: 'signTypedData', + name: 'Sign Typed Data', + extension: ['.json'], + multiselect: false, + label: '', + group: 3 }, { id: 'publishFolderToGist', name: 'Publish folder to gist',