pull/5298/head
yann300 2 months ago committed by Aniket
parent b75a95b35e
commit f385ffe1da
  1. 16
      apps/remix-ide-e2e/src/tests/signingMessage.test.ts
  2. 4
      apps/remix-ide/src/app/tabs/locales/en/filePanel.json
  3. 9
      apps/remix-ide/src/app/tabs/locales/en/udapp.json
  4. 22
      apps/remix-ide/src/blockchain/providers/vm.ts
  5. 5
      libs/remix-ui/run-tab/src/lib/actions/account.ts
  6. 3
      libs/remix-ui/run-tab/src/lib/actions/index.ts
  7. 51
      libs/remix-ui/run-tab/src/lib/components/account.tsx
  8. 1
      libs/remix-ui/run-tab/src/lib/components/settingsUI.tsx
  9. 4
      libs/remix-ui/run-tab/src/lib/run-tab.tsx
  10. 2
      libs/remix-ui/run-tab/src/lib/types/index.ts
  11. 29
      libs/remix-ui/workspace/src/lib/actions/index.tsx
  12. 5
      libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx
  13. 1
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  14. 6
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  15. 11
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  16. 2
      libs/remix-ui/workspace/src/lib/types/index.ts
  17. 7
      libs/remix-ui/workspace/src/lib/utils/index.ts

@ -51,7 +51,21 @@ module.exports = {
})
})
})
.end()
},
'Test EIP 712 Signature': function (browser: NightwatchBrowser) {
browser.waitForElementPresent('i[id="remixRunSignMsg"]')
.click('i[id="remixRunSignMsg"]')
.waitForElementVisible('*[data-id="signMessageTextarea"]', 120000)
.click('*[data-id="sign-eip-712"]')
.waitForElementVisible('*[data-id="udappNotify-modal-footer-ok-react"]')
.modalFooterOKClick('udappNotify')
.getEditorValue((content) => {
browser.assert.ok(content.indexOf('"primaryType": "AuthRequest",') !== -1, 'EIP 712 data file must be opened')
})
.rightClick('li[data-id="treeViewLitreeViewItemEIP-712-data.json"]')
.click('*[data-id="contextMenuItemsignTypedData"]')
.journalChildIncludes('0x248d23de0e23231370db8aa21ad5908ca90c33ae2b8c611b906674bda6b1a8b85813f945c2ea896316e240089029619ab3d801a1b098c199bd462dd8026349da1c')
}
}

@ -150,5 +150,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."
}

@ -46,7 +46,7 @@
"udapp._comment_account.tsx": "libs/remix-ui/run-tab/src/lib/components/account.tsx",
"udapp.account": "Account",
"udapp.signAMessage": "Sign a message",
"udapp.enterAMessageToSign": "Enter a message to sign",
"udapp.enterAMessageToSign": "Enter a message to sign and click `Sign`",
"udapp.hash": "hash",
"udapp.signature": "signature",
"udapp.injectedTitle": "Unfortunately it's not possible to create an account using injected provider. Please create the account directly from your provider (i.e metamask or other of the same type).",
@ -161,5 +161,10 @@
"udapp.ganacheProviderText1": "Note: To run Ganache on your system, run:",
"udapp.ganacheProviderText2": "For more info, visit: <a>Ganache Documentation</a>",
"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: <a>Hardhat Documentation</a>"
"udapp.hardhatProviderText2": "For more info, visit: <a>Hardhat Documentation</a>",
"udapp.EIP712-2": "Please follow <a>this link</a> 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",
"udapp.sign": "Sign"
}

@ -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<any>
}
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)

@ -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)
}

@ -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)

@ -111,7 +111,7 @@ export function AccountUI(props: AccountProps) {
props.modal(
intl.formatMessage({ id: 'udapp.signAMessage' }),
signMessagePrompt(),
intl.formatMessage({ id: 'udapp.ok' }),
intl.formatMessage({ id: 'udapp.sign' }),
() => {
props.signMessageWithAddress(selectedAccount, messageRef.current, signedMessagePrompt, props.passphrase)
props.setPassphrase('')
@ -130,7 +130,7 @@ export function AccountUI(props: AccountProps) {
props.modal(
intl.formatMessage({ id: 'udapp.signAMessage' }),
signMessagePrompt(),
intl.formatMessage({ id: 'udapp.ok' }),
intl.formatMessage({ id: 'udapp.sign' }),
() => {
props.signMessageWithAddress(selectedAccount, messageRef.current, signedMessagePrompt)
},
@ -167,7 +167,7 @@ export function AccountUI(props: AccountProps) {
<FormattedMessage id="udapp.enterAMessageToSign" />
<textarea
id="prompt_text"
className="bg-light text-light"
className="bg-light text-light form-control"
data-id="signMessageTextarea"
style={{ width: '100%' }}
rows={4}
@ -175,6 +175,25 @@ export function AccountUI(props: AccountProps) {
onInput={handleMessageInput}
defaultValue={messageRef.current}
></textarea>
<div className='mt-2'>
<span>otherwise</span><button className='ml-2 modal-ok btn btn-sm border-primary' data-id="sign-eip-712" onClick={() => {
props.modal(
'Message signing with EIP-712',
<div>
<div>{intl.formatMessage({ id: 'udapp.EIP712-2' }, {
a: (chunks) => (
<a href='https://eips.ethereum.org/EIPS/eip-712' target="_blank" rel="noreferrer">
{chunks}
</a>
)
})}</div>
<div>{intl.formatMessage({ id: 'udapp.EIP712-3' })}</div></div>,
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' }),
() => {})
}}>Sign with EIP 712</button>
</div>
</div>
)
}
@ -236,3 +255,29 @@ export function AccountUI(props: AccountProps) {
</div>
)
}
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' },
],
},
}

@ -15,6 +15,7 @@ export function SettingsUI(props: SettingsProps) {
<EnvironmentUI selectedEnv={props.selectExEnv} providers={props.providers} setExecutionContext={props.setExecutionContext} />
<NetworkUI networkName={props.networkName} />
<AccountUI
addFile={props.addFile}
personalMode={props.personalMode}
selectExEnv={props.selectExEnv}
accounts={props.accounts}

@ -47,7 +47,8 @@ import {
updateSelectedContract,
syncContracts,
isValidProxyAddress,
isValidProxyUpgrade
isValidProxyUpgrade,
addFile
} from './actions'
import './css/run-tab.css'
import { PublishToStorage } from '@remix-ui/publish-to-storage'
@ -280,6 +281,7 @@ export function RunTabUI(props: RunTabProps) {
<div className="udapp_runTabView run-tab" id="runTabView" data-id="runTabView">
<div className="list-group pb-4 list-group-flush">
<SettingsUI
addFile={addFile}
networkName={runTab.networkName}
personalMode={runTab.personalMode}
selectExEnv={runTab.selectExEnv}

@ -145,6 +145,7 @@ export interface SettingsProps {
isSuccessful: boolean,
error: string
},
addFile: (path: string, content: string) => 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,

@ -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,32 @@ 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 settings = await plugin.call('udapp', 'getSettings')
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: [settings.selectedAccount, parsed]
})
plugin.call('terminal', 'log', { type: 'log', value: `${path} signature using ${settings.selectedAccount} : ${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)
}

@ -41,6 +41,7 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
downloadPath,
uploadFile,
publishManyFilesToGist,
signTypedData,
...otherProps
} = props
const contextMenuRef = useRef(null)
@ -233,6 +234,10 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
case 'Publish Workspace to Gist':
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'publishWorkspace'])
publishFolderToGist(path)
break
case 'Sign Typed Data':
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'signTypedData'])
signTypedData(path)
break
default:
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', `${item.id}/${item.name}`])

@ -34,6 +34,7 @@ export const FileSystemContext = createContext<{
dispatchCopyShareURL: (path: string) => Promise<void>,
dispatchCopyFolder: (src: string, dest: string) => Promise<void>,
dispatchRunScript: (path: string) => Promise<void>,
dispatchSignTypedData: (path: string) => Promise<void>,
dispatchEmitContextMenuEvent: (cmd: customAction) => Promise<void>,
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' ) => Promise<void>
dispatchHandleExpandPath: (paths: string[]) => Promise<void>,

@ -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,

@ -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)
@ -1186,6 +1194,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}
@ -1262,6 +1271,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}
@ -1436,6 +1446,7 @@ export function Workspace() {
deletePath={deletePath}
renamePath={editModeOn}
runScript={runScript}
signTypedData={signTypedData}
copy={handleCopyClick}
paste={handlePasteClick}
copyFileName={handleCopyFileNameClick}

@ -128,6 +128,7 @@ export interface FileExplorerProps {
dispatchCopyShareURL: (path:string) => Promise<void>,
dispatchCopyFolder: (src: string, dest: string) => Promise<void>,
dispatchRunScript: (path: string) => Promise<void>,
dispatchSignTypedData: (path: string) => Promise<void>,
dispatchPublishToGist: (path?: string, type?: string) => Promise<void>,
dispatchEmitContextMenuEvent: (cmd: customAction) => Promise<void>,
dispatchHandleClickFile: (path: string, type: WorkspaceElement) => Promise<void>,
@ -197,6 +198,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

@ -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',

Loading…
Cancel
Save