diff --git a/packages/examples/create-react-app/package.json b/packages/examples/create-react-app/package.json index dc1f7aa21..e9d3fdf33 100644 --- a/packages/examples/create-react-app/package.json +++ b/packages/examples/create-react-app/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { - "@metamask/sdk-react": "^0.10.0", + "@metamask/sdk-react": "^0.10.1", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/packages/examples/create-react-app/src/App.css b/packages/examples/create-react-app/src/App.css index 74b5e0534..9519b44fa 100644 --- a/packages/examples/create-react-app/src/App.css +++ b/packages/examples/create-react-app/src/App.css @@ -7,6 +7,36 @@ pointer-events: none; } +.Button-Normal { + background-color: blueviolet; + border: 1px solid blueviolet; + color: white; + border-radius: 5px; + padding: 0.5rem 1rem; + font-size: 1rem; + cursor: pointer; +} + +.Button-Danger { + background-color: red; + border-color: red; + color: white; + border-radius: 5px; + padding: 0.5rem 1rem; + font-size: 1rem; + cursor: pointer; +} + +.Info-Status { + width: 90%; + margin-left: auto; + margin-right: auto; + margin-bottom: 2rem; + border-radius: 5px; + border: 1px solid blueviolet; + background-color: aliceblue; +} + @media (prefers-reduced-motion: no-preference) { .App-logo { animation: App-logo-spin infinite 20s linear; diff --git a/packages/examples/create-react-app/src/App.tsx b/packages/examples/create-react-app/src/App.tsx index 782edb5bb..e35250f8d 100644 --- a/packages/examples/create-react-app/src/App.tsx +++ b/packages/examples/create-react-app/src/App.tsx @@ -1,6 +1,7 @@ import { useSDK } from '@metamask/sdk-react'; import React, { useState } from 'react'; import './App.css'; +import { send_eth_signTypedData_v4, send_personal_sign } from './SignHelpers'; export const App = () => { const [response, setResponse] = useState(''); @@ -27,6 +28,17 @@ export const App = () => { }); }; + const connectAndSign = async () => { + try { + const signResult = await sdk?.connectAndSign({ + msg: 'Connect + Sign message' + }); + setResponse(signResult); + } catch (err) { + console.warn(`failed to connect..`, err); + } + }; + const connect = async () => { try { await sdk?.connect(); @@ -35,6 +47,24 @@ export const App = () => { } }; + const readOnlyCalls = async () => { + if(!sdk?.hasReadOnlyRPCCalls() && !provider){ + setResponse('readOnlyCalls are not set and provider is not set. Please set your infuraAPIKey in the SDK Options'); + return; + } + try { + const result = await provider.request({ + method: 'eth_blockNumber', + params: [], + }); + const gotFrom = sdk.hasReadOnlyRPCCalls() ? 'infura' : 'MetaMask provider'; + setResponse(`(${gotFrom}) ${result}`); + } catch (e) { + console.log(`error getting the blockNumber`, e); + setResponse('error getting the blockNumber'); + } + }; + const addEthereumChain = () => { if (!provider) { throw new Error(`invalid ethereum provider`); @@ -79,96 +109,22 @@ export const App = () => { } }; - const sign = async () => { - const msgParams = JSON.stringify({ - domain: { - // Defining the chain aka Rinkeby testnet or Ethereum Main Net - chainId: parseInt(provider?.chainId ?? '', 16), - // Give a user friendly name to the specific contract you are signing for. - name: 'Ether Mail', - // If name isn't enough add verifying contract to make sure you are establishing contracts with the proper entity - verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', - // Just let's you know the latest version. Definitely make sure the field name is correct. - version: '1', - }, - - // Defining the message signing data content. - message: { - /* - - Anything you want. Just a JSON Blob that encodes the data you want to send - - No required fields - - This is DApp Specific - - Be as explicit as possible when building out the message schema. - */ - contents: 'Hello, Bob!', - attachedMoneyInEth: 4.2, - from: { - name: 'Cow', - wallets: [ - '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', - '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF', - ], - }, - to: [ - { - name: 'Bob', - wallets: [ - '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', - '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57', - '0xB0B0b0b0b0b0B000000000000000000000000000', - ], - }, - ], - }, - // Refers to the keys of the *types* object below. - primaryType: 'Mail', - types: { - // TODO: Clarify if EIP712Domain refers to the domain the contract is hosted on - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - // Not an EIP712Domain definition - Group: [ - { name: 'name', type: 'string' }, - { name: 'members', type: 'Person[]' }, - ], - // Refer to PrimaryType - Mail: [ - { name: 'from', type: 'Person' }, - { name: 'to', type: 'Person[]' }, - { name: 'contents', type: 'string' }, - ], - // Not an EIP712Domain definition - Person: [ - { name: 'name', type: 'string' }, - { name: 'wallets', type: 'address[]' }, - ], - }, - }); - - let from = provider?.selectedAddress; - - console.debug(`sign from: ${from}`); - try { - if (!from || from === null) { - alert( - `Invalid account -- please connect using eth_requestAccounts first`, - ); - return; - } + const eth_signTypedData_v4 = async () => { + if (!provider) { + setResponse(`invalid ethereum provider`); + return; + } + const result = await send_eth_signTypedData_v4(provider, provider.chainId); + setResponse(result); + }; - const params = [from, msgParams]; - const method = 'eth_signTypedData_v4'; - console.debug(`ethRequest ${method}`, JSON.stringify(params, null, 4)); - console.debug(`sign params`, params); - const resp = await provider?.request({ method, params }); - setResponse(resp); - } catch (e) { - console.log(e); + const eth_personal_sign = async () => { + if (!provider) { + setResponse(`invalid ethereum provider`); + return; } + const result = await send_personal_sign(provider); + setResponse(result); }; const terminate = () => { @@ -190,6 +146,15 @@ export const App = () => { return (
+

Create-React-App Example

+
+

{`Connected chain: ${chainId}`}

+

{`Connected account: ${account}`}

+

{`Account balance: ${balance}`}

+

{`Last request response: ${response}`}

+

{`Connected: ${connected}`}

+
+
{connecting && (
Waiting for Metamask to link the connection...
@@ -212,59 +177,94 @@ export const App = () => { {connected ? (
- - - + { provider?.chainId === '0x1' ? ( + + ) : ( + + )} + + +
) : ( - +
+ + +
)} - -
-

{`Connected chain: ${chainId}`}

-

{`Connected account: ${account}`}

-

{`Account balance: ${balance}`}

-

{`Last request response: ${response}`}

-

{`Connected: ${connected}`}

-
); }; diff --git a/packages/examples/create-react-app/src/SignHelpers.ts b/packages/examples/create-react-app/src/SignHelpers.ts new file mode 100644 index 000000000..c0194f9e0 --- /dev/null +++ b/packages/examples/create-react-app/src/SignHelpers.ts @@ -0,0 +1,104 @@ +import { SDKProvider } from '@metamask/sdk'; +import { Buffer } from 'buffer'; + +export const send_eth_signTypedData_v4 = async (provider: SDKProvider, chainId: string) => { + const msgParams = JSON.stringify({ + domain: { + // Defining the chain aka Rinkeby testnet or Ethereum Main Net + chainId: chainId, + // Give a user-friendly name to the specific contract you are signing for. + name: 'Ether Mail', + // If name isn't enough add verifying contract to make sure you are establishing contracts with the proper entity + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + // Just lets you know the latest version. Definitely make sure the field name is correct. + version: '1', + }, + + message: { + contents: 'Hello, Bob!', + attachedMoneyInEth: 4.2, + from: { + name: 'Cow', + wallets: [ + '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF', + ], + }, + to: [ + { + name: 'Bob', + wallets: [ + '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57', + '0xB0B0b0b0b0b0B000000000000000000000000000', + ], + }, + ], + }, + // Refers to the keys of the *types* object below. + primaryType: 'Mail', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + // Not an EIP712Domain definition + Group: [ + { name: 'name', type: 'string' }, + { name: 'members', type: 'Person[]' }, + ], + // Refer to PrimaryType + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person[]' }, + { name: 'contents', type: 'string' }, + ], + // Not an EIP712Domain definition + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallets', type: 'address[]' }, + ], + }, + }); + + let from = provider?.selectedAddress; + + console.debug(`sign from: ${from}`); + try { + if (!from) { + alert( + `Invalid account -- please connect using eth_requestAccounts first`, + ); + return; + } + + const params = [from, msgParams]; + const method = 'eth_signTypedData_v4'; + console.debug(`ethRequest ${method}`, JSON.stringify(params, null, 4)); + console.debug(`sign params`, params); + return await provider?.request({ method, params }); + } catch (e) { + console.log(e); + return "Error: " + e.message; + } +}; + +export const send_personal_sign = async (provider: SDKProvider) => { + try { + const from = provider.selectedAddress; + const message = 'Hello World from the Create React dapp!'; + const hexMessage = '0x' + Buffer.from(message, 'utf8').toString('hex'); + + const sign = await window.ethereum.request({ + method: 'personal_sign', + params: [hexMessage, from, 'Example password'], + }); + console.log(`sign: ${sign}`); + return sign; + } catch (err) { + console.log(err); + return "Error: " + err.message; + } +}; diff --git a/packages/examples/electronjs/index.html b/packages/examples/electronjs/index.html index b6232185a..f612a4aaf 100644 --- a/packages/examples/electronjs/index.html +++ b/packages/examples/electronjs/index.html @@ -20,35 +20,36 @@ margin-top: 20px; } #buttonContainers { - margin-top: 20px; + margin-top: 20px; } .button { - min-width: 100px; - height: 50px; - border-radius: 5px; - border-color: transparent; + min-width: 100px; + height: 50px; + border-radius: 5px; + border-color: transparent; } .button.primary { - background-color: teal; - color: white + background-color: teal; + color: white } .button.action { - background-color: coral; - color: white; + background-color: coral; + color: white; + display: none; } .button.danger { - background-color: red; - color: white + background-color: red; + color: white } #response { - max-width: 80%; - overflow: auto; + max-width: 80%; + overflow: auto; } #otpContainer { - position: fixed; - bottom: 30px; - width: 100%; + position: fixed; + bottom: 30px; + width: 100%; } @@ -58,7 +59,11 @@

Electron Test Dapp

- + + + + +
diff --git a/packages/examples/electronjs/package.json b/packages/examples/electronjs/package.json index ef8a5cf99..c800c022d 100644 --- a/packages/examples/electronjs/package.json +++ b/packages/examples/electronjs/package.json @@ -13,7 +13,7 @@ "allow-scripts": "" }, "dependencies": { - "@metamask/sdk": "^0.10.0", + "@metamask/sdk": "^0.10.1", "qrcode": "^1.5.3", "typescript": "^5.1.6" } diff --git a/packages/examples/electronjs/src/sdk.ts b/packages/examples/electronjs/src/sdk.ts index a12623d3e..a68199432 100644 --- a/packages/examples/electronjs/src/sdk.ts +++ b/packages/examples/electronjs/src/sdk.ts @@ -33,12 +33,77 @@ const sdk = new MetaMaskSDK({ }, }); +const msgParams = { + domain: { + // Defining the chain aka Rinkeby testnet or Ethereum Main Net + chainId: '', + // Give a user-friendly name to the specific contract you are signing for. + name: 'Ether Mail', + // If name isn't enough add verifying contract to make sure you are establishing contracts with the proper entity + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + // Just lets you know the latest version. Definitely make sure the field name is correct. + version: '1', + }, + + message: { + contents: 'Hello, Bob!', + attachedMoneyInEth: 4.2, + from: { + name: 'Cow', + wallets: [ + '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF', + ], + }, + to: [ + { + name: 'Bob', + wallets: [ + '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57', + '0xB0B0b0b0b0b0B000000000000000000000000000', + ], + }, + ], + }, + // Refers to the keys of the *types* object below. + primaryType: 'Mail', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + // Not an EIP712Domain definition + Group: [ + { name: 'name', type: 'string' }, + { name: 'members', type: 'Person[]' }, + ], + // Refer to PrimaryType + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person[]' }, + { name: 'contents', type: 'string' }, + ], + // Not an EIP712Domain definition + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallets', type: 'address[]' }, + ], + }, +}; + // DOM Elements const qrCodeDOM = document.getElementById('qrCode'); const otpDOM = document.getElementById('otp'); -const signButtonDOM = document.getElementById('signButton'); +const signButtonDOM = document.getElementById('personalSignButton'); +const signTypedDataButtonDOM = document.getElementById('signTypedDataButton'); const connectButtonDOM = document.getElementById('connectButton'); +const switchChainDOM = document.getElementById('switchChainButton'); +const addPolygonDOM = document.getElementById('addChainButton'); +const switchPolygonDOM = document.getElementById('switchPolygonButton'); const terminateButtonDOM = document.getElementById('terminateButton'); const responseDOM = document.getElementById('response'); const accountsDOM = document.getElementById('account'); @@ -64,14 +129,18 @@ const connect = async () => { chainId = ethereum.chainId; updateDOM(chainDOM, chainId); signButtonDOM.style.display = 'inline'; + signTypedDataButtonDOM.style.display = 'inline'; + addPolygonDOM.style.display = 'inline'; + switchPolygonDOM.style.display = 'inline'; + switchChainDOM.style.display = 'inline'; }) .catch((error) => { console.error(error); }); }; -// Sign -const sign = async () => { +// Personal Sign +const personal_sign = async () => { const from = ethereum.selectedAddress; const message = 'Hello World from the Electron Example dapp!'; const hexMessage = '0x' + Buffer.from(message, 'utf8').toString('hex'); @@ -85,11 +154,77 @@ const sign = async () => { }).catch((e) => console.log('sign ERR', e)); }; +// eth_signTypedData_v4 +const eth_signTypedData_v4 = async () => { + let from = ethereum.selectedAddress; + try { + if (!from) { + alert( + `Invalid account -- please connect using eth_requestAccounts first`, + ); + return; + } + + msgParams.domain.chainId = ethereum.chainId; + const params = [from, JSON.stringify(msgParams)]; + const method = 'eth_signTypedData_v4'; + console.debug(`ethRequest ${method}`, JSON.stringify(params, null, 4)); + console.debug(`sign params`, params); + const result = await ethereum?.request({ method, params }); + updateDOM(responseDOM, result.toString()); + } catch (e) { + console.log(e); + return "Error: " + e.message; + } +}; + +// Chain Switch +const switchChain = async () => { + const currentChainId = ethereum.chainId; + const chainToSwitchTo = currentChainId === '0x1' ? '0x5' : '0x1'; + await ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: chainToSwitchTo }], + }); +}; + +// Switch to Polygon +const switchToPolygon = async () => { + const chainToSwitchTo = '0x89'; + await ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: chainToSwitchTo }], + }); +}; + +// Add Polygon Chain +const addPolygonChain = async () => { + ethereum + .request({ + method: 'wallet_addEthereumChain', + params: [ + { + chainId: '0x89', + chainName: 'Polygon', + blockExplorerUrls: ['https://polygonscan.com'], + nativeCurrency: { symbol: 'MATIC', decimals: 18 }, + rpcUrls: ['https://polygon-rpc.com/'], + }, + ], + }) + .then((res) => console.log('add', res)) + .catch((e) => console.log('ADD ERR', e)); +}; + // Terminate const terminate = () => { sdk.terminate(); connectButtonDOM.textContent = 'Connect'; signButtonDOM.style.display = 'none'; + signTypedDataButtonDOM.style.display = 'none'; + switchChainDOM.style.display = 'none'; + addPolygonDOM.style.display = 'none'; + switchPolygonDOM.style.display = 'none'; accountsDOM.innerText = ''; chainDOM.innerText = ''; qrCodeDOM.innerText = ''; @@ -101,7 +236,11 @@ const terminate = () => { // Event listeners connectButtonDOM.onclick = connect; connectButtonDOM.addEventListener('click', connect); -signButtonDOM.addEventListener('click', sign); +signButtonDOM.addEventListener('click', personal_sign); +signTypedDataButtonDOM.addEventListener('click', eth_signTypedData_v4); +switchChainDOM.addEventListener('click', switchChain); +addPolygonDOM.addEventListener('click', addPolygonChain); +switchPolygonDOM.addEventListener('click', switchToPolygon); terminateButtonDOM.addEventListener('click', terminate); @@ -135,6 +274,7 @@ const setEventListeners = () => { ethereum.on('connect', () => { qrCodeDOM.innerText = ''; + signButtonDOM.style.display = 'inline'; if (account !== '') { updateDOM(otpDOM, ''); } diff --git a/packages/examples/nextjs-demo/package.json b/packages/examples/nextjs-demo/package.json index 1588eb3c0..667d748a3 100644 --- a/packages/examples/nextjs-demo/package.json +++ b/packages/examples/nextjs-demo/package.json @@ -10,7 +10,7 @@ "allow-scripts": "echo 'n/a'" }, "dependencies": { - "@metamask/sdk-react": "^0.10.0", + "@metamask/sdk-react": "^0.10.1", "@types/node": "20.2.1", "@types/react": "18.2.6", "@types/react-dom": "18.2.4", diff --git a/packages/examples/nextjs-demo/src/app/SDKContainer.css b/packages/examples/nextjs-demo/src/app/SDKContainer.css new file mode 100644 index 000000000..d65cc8fc9 --- /dev/null +++ b/packages/examples/nextjs-demo/src/app/SDKContainer.css @@ -0,0 +1,36 @@ +.info-section { + width: 90%; + margin-left: auto; + margin-right: auto; + background-color: #fcdab2; + border: 1px solid #947549; + border-radius: 5px; + text-align: center; + word-break: break-word; + margin-top: 15px; + padding: 15px; +} + +main { + width: 100vw; +} + +.button-normal { + background-color: #947549; + border: 1px solid #947549; + color: white; + border-radius: 5px; + padding: 0.5rem 1rem; + font-size: 1rem; + cursor: pointer; +} + +.button-danger { + background-color: red; + border: 1px solid red; + color: white; + border-radius: 5px; + padding: 0.5rem 1rem; + font-size: 1rem; + cursor: pointer; +} diff --git a/packages/examples/nextjs-demo/src/app/SDKContainer.tsx b/packages/examples/nextjs-demo/src/app/SDKContainer.tsx index a6999aa7b..3c201da2b 100644 --- a/packages/examples/nextjs-demo/src/app/SDKContainer.tsx +++ b/packages/examples/nextjs-demo/src/app/SDKContainer.tsx @@ -8,6 +8,11 @@ import { } from '@metamask/sdk-communication-layer'; import Head from 'next/head'; import { useEffect, useState } from 'react'; +import './SDKContainer.css'; +import { + send_eth_signTypedData_v4, + send_personal_sign, +} from '@/app/SignHelpers'; declare global { interface Window { @@ -15,33 +20,34 @@ declare global { } } + export default function SDKContainer() { const [sdk, setSDK] = useState(); const [chain, setChain] = useState(''); - const [account, setAccount] = useState(); + const [account, setAccount] = useState(''); const [response, setResponse] = useState(''); const [connected, setConnected] = useState(false); const [serviceStatus, setServiceStatus] = useState(); const [activeProvider, setActiveProvider] = useState(); + const [currentLanguage, setCurrentLanguage] = useState( + localStorage.getItem('MetaMaskSDKLng') || 'en', + ); - const connect = () => { - if (!window.ethereum) { - throw new Error(`invalid ethereum provider`); - } + const languages = sdk?.availableLanguages ?? ['en']; - window.ethereum - .request({ - method: 'eth_requestAccounts', - params: [], - }) - .then((accounts) => { - if (accounts) { - console.debug(`connect:: accounts result`, accounts); - setAccount((accounts as string[])[0]); - setConnected(true); - } - }) - .catch((e) => console.log('request accounts ERR', e)); + const changeLanguage = async (currentLanguage: string) => { + localStorage.setItem('MetaMaskSDKLng', currentLanguage); + window.location.reload(); + }; + + const handleLanguageChange = ( + event: React.ChangeEvent, + ) => { + setCurrentLanguage(event.target.value); + + changeLanguage(event.target.value).then(() => { + console.debug(`language changed to ${event.target.value}`); + }); }; useEffect(() => { @@ -50,6 +56,9 @@ export default function SDKContainer() { useDeeplink: false, communicationServerUrl: process.env.NEXT_PUBLIC_COMM_SERVER_URL, checkInstallationImmediately: false, + i18nOptions: { + enabled: true + }, dappMetadata: { name: 'NEXTJS demo', url: window.location.host, @@ -106,9 +115,10 @@ export default function SDKContainer() { setConnected(true); }; - const onConnect = (_connectInfo: unknown) => { + const onConnect = (_connectInfo: any) => { console.log(`App::useEfect on 'connect'`, _connectInfo); setConnected(true); + setChain(_connectInfo.chainId as string); }; const onDisconnect = (error: unknown) => { @@ -156,7 +166,7 @@ export default function SDKContainer() { setAccount(accounts?.[0]); } else { setConnected(false); - setAccount(undefined); + setAccount(''); } setActiveProvider(sdk.getProvider()); }; @@ -167,103 +177,139 @@ export default function SDKContainer() { }; }, [sdk]); - const eth_signTypedData_v4 = async () => { - const msgParams = JSON.stringify({ - domain: { - // Defining the chain aka Rinkeby testnet or Ethereum Main Net - chainId: parseInt(window.ethereum?.chainId ?? '', 16), - // Give a user friendly name to the specific contract you are signing for. - name: 'Ether Mail', - // If name isn't enough add verifying contract to make sure you are establishing contracts with the proper entity - verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', - // Just let's you know the latest version. Definitely make sure the field name is correct. - version: '1', - }, - - // Defining the message signing data content. - message: { - /* - - Anything you want. Just a JSON Blob that encodes the data you want to send - - No required fields - - This is DApp Specific - - Be as explicit as possible when building out the message schema. - */ - contents: 'Hello, Bob!', - attachedMoneyInEth: 4.2, - from: { - name: 'Cow', - wallets: [ - '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', - '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF', - ], - }, - to: [ - { - name: 'Bob', - wallets: [ - '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', - '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57', - '0xB0B0b0b0b0b0B000000000000000000000000000', - ], - }, - ], - }, - // Refers to the keys of the *types* object below. - primaryType: 'Mail', - types: { - // TODO: Clarify if EIP712Domain refers to the domain the contract is hosted on - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - // Not an EIP712Domain definition - Group: [ - { name: 'name', type: 'string' }, - { name: 'members', type: 'Person[]' }, - ], - // Refer to PrimaryType - Mail: [ - { name: 'from', type: 'Person' }, - { name: 'to', type: 'Person[]' }, - { name: 'contents', type: 'string' }, - ], - // Not an EIP712Domain definition - Person: [ - { name: 'name', type: 'string' }, - { name: 'wallets', type: 'address[]' }, - ], - }, - }); + const connect = () => { + if (!window.ethereum) { + throw new Error(`invalid ethereum provider`); + } - const from = window.ethereum?.selectedAddress; + window.ethereum + .request({ + method: 'eth_requestAccounts', + params: [], + }) + .then((accounts) => { + if (accounts) { + console.debug(`connect:: accounts result`, accounts); + setAccount((accounts as string[])[0]); + setConnected(true); + } + }) + .catch((e) => console.log('request accounts ERR', e)); + }; - console.debug(`sign from: ${from}`); + const connectAndSign = async () => { try { - if (!from || from === null) { - alert( - `Invalid account -- please connect using eth_requestAccounts first`, - ); - return; - } + const signResult = await sdk?.connectAndSign({ + msg: 'Connect + Sign message' + }); + setResponse(signResult); + setAccount(window.ethereum?.selectedAddress ?? ''); + setConnected(true); + setChain(window.ethereum?.chainId ?? ''); + } catch (err) { + console.warn(`failed to connect..`, err); + } + }; + + const eth_signTypedData_v4 = async () => { + if (!activeProvider || !activeProvider.chainId) { + setResponse(`invalid ethereum provider`); + return; + } + const result = await send_eth_signTypedData_v4(activeProvider, activeProvider.chainId); + setResponse(result); + }; - const params = [from, msgParams]; - const method = 'eth_signTypedData_v4'; - console.debug(`ethRequest ${method}`, JSON.stringify(params, null, 4)); - console.debug(`sign params`, params); - const resp = (await window.ethereum?.request({ - method, - params, + const eth_personal_sign = async () => { + if (!activeProvider) { + setResponse(`invalid ethereum provider`); + return; + } + const result = await send_personal_sign(activeProvider); + setResponse(result); + }; + + const sendTransaction = async () => { + const to = '0x0000000000000000000000000000000000000000'; + const transactionParameters = { + to, // Required except during contract publications. + from: activeProvider?.selectedAddress, // must match user's active address. + value: '0x5AF3107A4000', // Only required to send ether to the recipient from the initiating external account. + }; + + try { + // txHash is a hex string + // As with any RPC call, it may throw an error + const txHash = (await activeProvider?.request({ + method: 'eth_sendTransaction', + params: [transactionParameters], })) as string; - setResponse(resp); + + setResponse(txHash); } catch (e) { console.log(e); } }; + const changeNetwork = async (hexChainId: string) => { + console.debug(`switching to network chainId=${hexChainId}`); + try { + const response = await activeProvider?.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: hexChainId }], // chainId must be in hexadecimal numbers + }); + console.debug(`response`, response); + } catch (err) { + console.error(err); + } + }; + + const addEthereumChain = () => { + if (!activeProvider) { + throw new Error(`invalid ethereum provider`); + } + + activeProvider + .request({ + method: 'wallet_addEthereumChain', + params: [ + { + chainId: '0x89', + chainName: 'Polygon', + blockExplorerUrls: ['https://polygonscan.com'], + nativeCurrency: { symbol: 'MATIC', decimals: 18 }, + rpcUrls: ['https://polygon-rpc.com/'], + }, + ], + }) + .then((res) => console.log('add', res)) + .catch((e) => console.log('ADD ERR', e)); + }; + + const readOnlyCalls = async () => { + if(!sdk?.hasReadOnlyRPCCalls() && window.ethereum === undefined){ + setResponse('readOnlyCalls are not set and provider is not set. Please set your infuraAPIKey in the SDK Options'); + return; + } + try { + const result = await window.ethereum?.request({ + method: 'eth_blockNumber', + params: [], + }); + console.log(`got blockNumber`, result) + const gotFrom = sdk!!.hasReadOnlyRPCCalls() ? 'infura' : 'MetaMask provider'; + setResponse(`(${gotFrom}) ${result}`); + } catch (e) { + console.log(`error getting the blockNumber`, e); + setResponse('error getting the blockNumber'); + } + }; + const terminate = () => { sdk?.terminate(); + setChain(''); + setAccount(''); + setResponse(''); }; return ( @@ -274,55 +320,139 @@ export default function SDKContainer() { -
- {serviceStatus?.connectionStatus === ConnectionStatus.WAITING && ( -
Waiting for Metamask to link the connection...
- )} -

ChannelId: {serviceStatus?.channelConfig?.channelId}

-

{`Expiration: ${serviceStatus?.channelConfig?.validUntil ?? ''}`}

- - {connected ? ( -
- - - -
- ) : ( - - )} - - - -
+
+

NextJS Example

+
+ + +
+
<> - {chain && `Connected chain: ${chain}`} + {`Connected chain: ${chain}`} +

+ {`Connected account: ${account}`} +

+ {`Response: ${response}`}

- {account && `Connected account: ${account}`} -

- {response && `Last request response: ${response}`} -

+ {`Connected: ${connected}`} + + {serviceStatus?.connectionStatus === ConnectionStatus.WAITING && ( +
Waiting for Metamask to link the connection...
+ )} +

ChannelId: {serviceStatus?.channelConfig?.channelId}

+

{`Expiration: ${serviceStatus?.channelConfig?.validUntil ?? ''}`}

+ + + +
+ {connected ? ( +
+ + + + + + + + + { activeProvider?.chainId === '0x1' ? ( + + ) : ( + + )} + + + + + + +
+ ) : ( + <> + + + + + + )} + + +
); diff --git a/packages/examples/nextjs-demo/src/app/SignHelpers.ts b/packages/examples/nextjs-demo/src/app/SignHelpers.ts new file mode 100644 index 000000000..a071751f7 --- /dev/null +++ b/packages/examples/nextjs-demo/src/app/SignHelpers.ts @@ -0,0 +1,102 @@ +import { SDKProvider } from '@metamask/sdk'; +import { Buffer } from 'buffer'; + +export const send_eth_signTypedData_v4 = async (provider: SDKProvider, chainId: string) => { + const msgParams = JSON.stringify({ + domain: { + // Defining the chain aka Rinkeby testnet or Ethereum Main Net + chainId: chainId, + // Give a user-friendly name to the specific contract you are signing for. + name: 'Ether Mail', + // If name isn't enough add verifying contract to make sure you are establishing contracts with the proper entity + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + // Just lets you know the latest version. Definitely make sure the field name is correct. + version: '1', + }, + + message: { + contents: 'Hello, Bob!', + attachedMoneyInEth: 4.2, + from: { + name: 'Cow', + wallets: [ + '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF', + ], + }, + to: [ + { + name: 'Bob', + wallets: [ + '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57', + '0xB0B0b0b0b0b0B000000000000000000000000000', + ], + }, + ], + }, + // Refers to the keys of the *types* object below. + primaryType: 'Mail', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + // Not an EIP712Domain definition + Group: [ + { name: 'name', type: 'string' }, + { name: 'members', type: 'Person[]' }, + ], + // Refer to PrimaryType + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person[]' }, + { name: 'contents', type: 'string' }, + ], + // Not an EIP712Domain definition + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallets', type: 'address[]' }, + ], + }, + }); + + let from = provider?.selectedAddress; + + console.debug(`sign from: ${from}`); + try { + if (!from) { + alert( + `Invalid account -- please connect using eth_requestAccounts first`, + ); + return; + } + + const params = [from, msgParams]; + const method = 'eth_signTypedData_v4'; + console.debug(`ethRequest ${method}`, JSON.stringify(params, null, 4)); + console.debug(`sign params`, params); + return await provider?.request({ method, params }); + } catch (err) { + console.log(err); + } +}; + +export const send_personal_sign = async (provider: SDKProvider) => { + try { + const from = provider.selectedAddress; + const message = 'Hello World from the NextJS dapp!'; + const hexMessage = '0x' + Buffer.from(message, 'utf8').toString('hex'); + + const sign = await window.ethereum?.request({ + method: 'personal_sign', + params: [hexMessage, from, 'Example password'], + }); + console.log(`sign: ${sign}`); + return sign; + } catch (err) { + console.log(err); + } +}; diff --git a/packages/examples/nextjs-demo/src/app/globals.css b/packages/examples/nextjs-demo/src/app/globals.css index d4f491e15..539093c41 100644 --- a/packages/examples/nextjs-demo/src/app/globals.css +++ b/packages/examples/nextjs-demo/src/app/globals.css @@ -1,77 +1,10 @@ :root { - --max-width: 1100px; - --border-radius: 12px; --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; - - --primary-glow: conic-gradient( - from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg - ); - --secondary-glow: radial-gradient( - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 0) - ); - - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient( - #00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080 - ); - - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; } -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - - --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient( - to bottom right, - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0.3) - ); - - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient( - #ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80 - ); - - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; - } -} * { box-sizing: border-box; @@ -81,18 +14,14 @@ html, body { + margin:0; max-width: 100vw; overflow-x: hidden; } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + color: #000000; + background: #FFFFFF; } a { @@ -100,8 +29,8 @@ a { text-decoration: none; } -@media (prefers-color-scheme: dark) { +@media (prefers-color-scheme: light) { html { - color-scheme: dark; + color-scheme: light; } } diff --git a/packages/examples/nextjs-demo/src/app/page.module.css b/packages/examples/nextjs-demo/src/app/page.module.css index 9411a5e6f..17a3e7b21 100644 --- a/packages/examples/nextjs-demo/src/app/page.module.css +++ b/packages/examples/nextjs-demo/src/app/page.module.css @@ -7,223 +7,3 @@ min-height: 100vh; } -.description { - display: inherit; - justify-content: inherit; - align-items: inherit; - font-size: 0.85rem; - max-width: var(--max-width); - width: 100%; - z-index: 2; - font-family: var(--font-mono); -} - -.description a { - display: flex; - justify-content: center; - align-items: center; - gap: 0.5rem; -} - -.description p { - position: relative; - margin: 0; - padding: 1rem; - background-color: rgba(var(--callout-rgb), 0.5); - border: 1px solid rgba(var(--callout-border-rgb), 0.3); - border-radius: var(--border-radius); -} - -.code { - font-weight: 700; - font-family: var(--font-mono); -} - -.grid { - display: grid; - grid-template-columns: repeat(4, minmax(25%, auto)); - width: var(--max-width); - max-width: 100%; -} - -.card { - padding: 1rem 1.2rem; - border-radius: var(--border-radius); - background: rgba(var(--card-rgb), 0); - border: 1px solid rgba(var(--card-border-rgb), 0); - transition: background 200ms, border 200ms; -} - -.card span { - display: inline-block; - transition: transform 200ms; -} - -.card h2 { - font-weight: 600; - margin-bottom: 0.7rem; -} - -.card p { - margin: 0; - opacity: 0.6; - font-size: 0.9rem; - line-height: 1.5; - max-width: 30ch; -} - -.center { - display: flex; - justify-content: center; - align-items: center; - position: relative; - padding: 4rem 0; -} - -.center::before { - background: var(--secondary-glow); - border-radius: 50%; - width: 480px; - height: 360px; - margin-left: -400px; -} - -.center::after { - background: var(--primary-glow); - width: 240px; - height: 180px; - z-index: -1; -} - -.center::before, -.center::after { - content: ''; - left: 50%; - position: absolute; - filter: blur(45px); - transform: translateZ(0); -} - -.logo { - position: relative; -} -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - .card:hover { - background: rgba(var(--card-rgb), 0.1); - border: 1px solid rgba(var(--card-border-rgb), 0.15); - } - - .card:hover span { - transform: translateX(4px); - } -} - -@media (prefers-reduced-motion) { - .card:hover span { - transform: none; - } -} - -/* Mobile */ -@media (max-width: 700px) { - .content { - padding: 4rem; - } - - .grid { - grid-template-columns: 1fr; - margin-bottom: 120px; - max-width: 320px; - text-align: center; - } - - .card { - padding: 1rem 2.5rem; - } - - .card h2 { - margin-bottom: 0.5rem; - } - - .center { - padding: 8rem 0 6rem; - } - - .center::before { - transform: none; - height: 300px; - } - - .description { - font-size: 0.8rem; - } - - .description a { - padding: 1rem; - } - - .description p, - .description div { - display: flex; - justify-content: center; - position: fixed; - width: 100%; - } - - .description p { - align-items: center; - inset: 0 0 auto; - padding: 2rem 1rem 1.4rem; - border-radius: 0; - border: none; - border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); - background: linear-gradient( - to bottom, - rgba(var(--background-start-rgb), 1), - rgba(var(--callout-rgb), 0.5) - ); - background-clip: padding-box; - backdrop-filter: blur(24px); - } - - .description div { - align-items: flex-end; - pointer-events: none; - inset: auto 0 0; - padding: 2rem; - height: 200px; - background: linear-gradient( - to bottom, - transparent 0%, - rgb(var(--background-end-rgb)) 40% - ); - z-index: 1; - } -} - -/* Tablet and Smaller Desktop */ -@media (min-width: 701px) and (max-width: 1120px) { - .grid { - grid-template-columns: repeat(2, 50%); - } -} - -@media (prefers-color-scheme: dark) { - .vercelLogo { - filter: invert(1); - } - - .logo { - filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); - } -} - -@keyframes rotate { - from { - transform: rotate(360deg); - } - to { - transform: rotate(0deg); - } -} diff --git a/packages/examples/nextjs-demo/yarn.lock b/packages/examples/nextjs-demo/yarn.lock index 4ad510191..94ab4d940 100644 --- a/packages/examples/nextjs-demo/yarn.lock +++ b/packages/examples/nextjs-demo/yarn.lock @@ -418,9 +418,9 @@ __metadata: languageName: node linkType: hard -"@metamask/sdk-communication-layer@npm:0.10.0": - version: 0.10.0 - resolution: "@metamask/sdk-communication-layer@npm:0.10.0" +"@metamask/sdk-communication-layer@npm:0.10.1": + version: 0.10.1 + resolution: "@metamask/sdk-communication-layer@npm:0.10.1" dependencies: cross-fetch: ^3.1.5 date-fns: ^2.29.3 @@ -428,7 +428,7 @@ __metadata: eventemitter2: ^6.4.5 socket.io-client: ^4.5.1 uuid: ^8.3.2 - checksum: 0bf3702e137f8fdbc3f4ac7242f83dc741083cda7ee0102f6d9c2044405e925e14e411014350a2c8068f4e110c1172613161433b3d61d968ea3b615f9cc0929b + checksum: 12976e562c4e9da65c52fb8d4f14305653557d4ccb5721ad1de87f9a08d7679503dc2fb92c8330b30cae4c4aa5cab05399100d6590e68e4e614c9b9560602c1d languageName: node linkType: hard @@ -447,26 +447,26 @@ __metadata: languageName: node linkType: hard -"@metamask/sdk-react@npm:^0.10.0": - version: 0.10.0 - resolution: "@metamask/sdk-react@npm:0.10.0" +"@metamask/sdk-react@npm:^0.10.1": + version: 0.10.1 + resolution: "@metamask/sdk-react@npm:0.10.1" dependencies: - "@metamask/sdk": ^0.10.0 + "@metamask/sdk": ^0.10.1 peerDependencies: react: ^18.2.0 react-dom: ^18.2.0 - checksum: ee3a998ff4355740c0395c8a49d305aafb1133a593678b27e27071911e2b3aae44ffdb00fd765703dcb0b08efdca2f718092e6608b26bc5f34014972282b2967 + checksum: 5651610d1182da4fb93683adb1779b90823f1f27fc0435f0ca0bc5f5be2983b45aad4ef2534585fcd9b5a7f1f7813e7f9da4c31e530e6a6a7b2b5b9116d1a7c4 languageName: node linkType: hard -"@metamask/sdk@npm:^0.10.0": - version: 0.10.0 - resolution: "@metamask/sdk@npm:0.10.0" +"@metamask/sdk@npm:^0.10.1": + version: 0.10.1 + resolution: "@metamask/sdk@npm:0.10.1" dependencies: "@metamask/onboarding": ^1.0.1 "@metamask/post-message-stream": ^6.1.0 "@metamask/providers": ^10.2.1 - "@metamask/sdk-communication-layer": 0.10.0 + "@metamask/sdk-communication-layer": 0.10.1 "@metamask/sdk-install-modal-web": 0.10.0 "@react-native-async-storage/async-storage": ^1.17.11 "@types/dom-screen-wake-lock": ^1.0.0 @@ -495,7 +495,7 @@ __metadata: optional: true react-native: optional: true - checksum: a1c9f0e3bce0b682cdd1dd51bedec7229bf800655fea07e03e2686ea1a5441bd355c9e4e9aa63150fffa623ea467c463f9cf991f1a21855c9b85a448e2e3930d + checksum: 461a7db50533c11b2c2c7fb992d5f09204a095b622693163c9d8a936046a31c51ebf6526f1bc602619256ef8c88bf2af76c37da2eda03a78d52ed2cac7ed05e8 languageName: node linkType: hard @@ -3513,7 +3513,7 @@ __metadata: version: 0.0.0-use.local resolution: "nextjs-demo@workspace:." dependencies: - "@metamask/sdk-react": ^0.10.0 + "@metamask/sdk-react": ^0.10.1 "@types/node": 20.2.1 "@types/react": 18.2.6 "@types/react-dom": 18.2.4 diff --git a/packages/examples/nodejs/package.json b/packages/examples/nodejs/package.json index 745e7c2ff..62d32299a 100644 --- a/packages/examples/nodejs/package.json +++ b/packages/examples/nodejs/package.json @@ -7,14 +7,15 @@ "private": true, "main": "index.js", "scripts": { - "start": "ts-node src/index.ts", + "start:connect": "ts-node src/index.ts connect", + "start:sign": "ts-node src/index.ts sign", "build": "tsc", "clean": "rm -rf dist .sdk-comm", "test": "echo \"Error: no test specified\" && exit 1", "allow-scripts": "echo 'n/a'" }, "dependencies": { - "@metamask/sdk": "^0.10.0", + "@metamask/sdk": "^0.10.1", "qrcode-terminal": "^0.12.0" }, "devDependencies": { diff --git a/packages/examples/nodejs/src/index.ts b/packages/examples/nodejs/src/index.ts index d4daf57c5..3d579338d 100644 --- a/packages/examples/nodejs/src/index.ts +++ b/packages/examples/nodejs/src/index.ts @@ -74,27 +74,29 @@ const msgParams = { }, }; -const start = async () => { +const start = async (startType) => { console.debug(`start NodeJS example`); - const accounts = await sdk.connect(); - - console.log('connect request accounts', accounts); + if (startType === 'connect') { + const accounts = await sdk.connect(); + console.log('connect request accounts', accounts); + } else { + const hexSign = await sdk.connectAndSign({msg: "Hello from the NodeJS Example!"}) + console.log('connect and sign', hexSign); + } const ethereum = sdk.getProvider(); - ethereum.on('_initialized', async () => { - const from = accounts?.[0]; - - const signResponse = await ethereum.request({ - method: 'eth_signTypedData_v3', - params: [from, JSON.stringify(msgParams)], - }); - - console.log('sign response', signResponse); + const signResponse = await ethereum.request({ + method: 'eth_signTypedData_v3', + params: [ethereum.selectedAddress, JSON.stringify(msgParams)], }); + + console.log('eth_signTypedData_v3 response', signResponse); }; -start().catch((err) => { +const startType = process.argv[2]; + +start(startType).catch((err) => { console.error(err); }); diff --git a/packages/examples/vuejs/package.json b/packages/examples/vuejs/package.json index f1afa04c6..3cd9e6570 100644 --- a/packages/examples/vuejs/package.json +++ b/packages/examples/vuejs/package.json @@ -9,7 +9,7 @@ "allow-scripts": "echo 'n/a'" }, "dependencies": { - "@metamask/sdk": "^0.10.0", + "@metamask/sdk": "^0.10.1", "core-js": "^3.8.3", "vue": "^3.2.13" }, diff --git a/packages/examples/vuejs/src/components/MetaMaskComponent.vue b/packages/examples/vuejs/src/components/MetaMaskComponent.vue index 41476181c..625f4f2a7 100644 --- a/packages/examples/vuejs/src/components/MetaMaskComponent.vue +++ b/packages/examples/vuejs/src/components/MetaMaskComponent.vue @@ -1,28 +1,31 @@ @@ -35,10 +38,13 @@ export default { data() { return { sdk: null, - accounts: null, + account: null, chainId: null, connected: false, lastResponse: null, + provider: null, + availableLanguages: [], + selectedLanguage: '', }; }, created() { @@ -48,73 +54,76 @@ export default { name: 'MetaMask VueJS Example Dapp', }, enableDebug: true, - autoConnect: { - enable: true, - }, + checkInstallationImmediately: true, logging: { developerMode: true, }, - storage: { + i18nOptions: { enabled: true, }, }); }, - mounted() { - if (this.sdk?.isInitialized()) { + async mounted() { + // Init SDK + await this.sdk?.init().then(() => { + this.provider = this.sdk?.getProvider(); // Chain changed - window.ethereum?.on("chainChanged", (chain) => { + this.provider?.on("chainChanged", (chain) => { console.log(`App::Chain changed:'`, chain); this.chainId = chain; }); // Accounts changed - window.ethereum?.on("accountsChanged", (accounts) => { + this.provider?.on("accountsChanged", (accounts) => { console.log(`App::Accounts changed:'`, accounts); - this.accounts = accounts; - }); - - // Initialized event - window.ethereum?.on('_initialized', () => { - console.debug(`App::useEffect on _initialized`); - // Getting the accounts again to display in the UI - this.onConnect(); - if (window.ethereum?.chainId) { - this.chainId = window.ethereum.chainId; - } + this.account = accounts[0]; }); // Connected event - window.ethereum?.on('connect', (_connectInfo) => { + this.provider?.on('connect', (_connectInfo) => { console.log(`App::connect`, _connectInfo); + this.onConnect(); this.connected = true; }); // Disconnect event - window.ethereum?.on('disconnect', (error) => { + this.provider?.on('disconnect', (error) => { console.log(`App::disconnect`, error); this.connected = false; }); - } + + this.availableLanguages = this.sdk?.availableLanguages ?? ['en'] + }); }, methods: { + async onConnectAndSign() { + try { + const signResult = await this.sdk?.connectAndSign({ + msg: 'Connect + Sign message' + }); + this.lastResponse = signResult; + } catch (err) { + console.warn(`failed to connect..`, err); + } + }, async onConnect() { try { - const res = await window.ethereum.request({ + const res = await this.provider.request({ method: 'eth_requestAccounts', params: [], }); - this.accounts = res; + this.account = res[0]; console.log('request accounts', res); this.lastResponse = ""; - this.chainId = window.ethereum.chainId; + this.chainId = this.provider.chainId; } catch (e) { console.log('request accounts ERR', e); } }, async addEthereumChain() { try { - const res = await window.ethereum.request({ + const res = await this.provider.request({ method: 'wallet_addEthereumChain', params: [ { @@ -132,25 +141,165 @@ export default { console.log('ADD ERR', e); } }, - async onSign() { + async eth_signTypedData_v4() { + const msgParams = JSON.stringify({ + domain: { + // Defining the chain aka Rinkeby testnet or Ethereum Main Net + chainId: this.chainId, + // Give a user-friendly name to the specific contract you are signing for. + name: 'Ether Mail', + // If name isn't enough add verifying contract to make sure you are establishing contracts with the proper entity + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + // Just lets you know the latest version. Definitely make sure the field name is correct. + version: '1', + }, + + message: { + contents: 'Hello, Bob!', + attachedMoneyInEth: 4.2, + from: { + name: 'Cow', + wallets: [ + '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF', + ], + }, + to: [ + { + name: 'Bob', + wallets: [ + '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57', + '0xB0B0b0b0b0b0B000000000000000000000000000', + ], + }, + ], + }, + // Refers to the keys of the *types* object below. + primaryType: 'Mail', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + // Not an EIP712Domain definition + Group: [ + { name: 'name', type: 'string' }, + { name: 'members', type: 'Person[]' }, + ], + // Refer to PrimaryType + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person[]' }, + { name: 'contents', type: 'string' }, + ], + // Not an EIP712Domain definition + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallets', type: 'address[]' }, + ], + }, + }); + + let from = this.account; + + console.debug(`sign from: ${from}`); + try { + if (!from) { + alert( + `Invalid account -- please connect using eth_requestAccounts first`, + ); + return; + } + + const params = [from, msgParams]; + const method = 'eth_signTypedData_v4'; + console.debug(`ethRequest ${method}`, JSON.stringify(params, null, 4)); + console.debug(`sign params`, params); + const result = await this.provider?.request({ method, params }); + this.lastResponse = result; + console.debug(`eth_signTypedData_v4 result`, result); + } catch (e) { + this.lastResponse = e; + console.error(e); + } + }, + async personal_sign() { try { - const from = window.ethereum?.selectedAddress; - const message = 'Hello World from the Vue Example dapp!'; + const from = this.provider.selectedAddress; + const message = 'Hello World from the VueJS Example dapp!'; const hexMessage = '0x' + Buffer.from(message, 'utf8').toString('hex'); const sign = await window.ethereum.request({ method: 'personal_sign', params: [hexMessage, from, 'Example password'], }); - console.log(sign); - this.lastResponse = sign; + console.log(`sign: ${sign}`); + return sign; + } catch (err) { + console.log(err); + return "Error: " + err.message; + } + }, + async sendTransaction() { + const to = '0x0000000000000000000000000000000000000000'; + const transactionParameters = { + to, // Required except during contract publications. + from: this.provider.selectedAddress, // must match user's active address. + value: '0x5AF3107A4000', // Only required to send ether to the recipient from the initiating external account. + }; + + try { + // txHash is a hex string + // As with any RPC call, it may throw an error + const txHash = (await this.provider.request({ + method: 'eth_sendTransaction', + params: [transactionParameters], + })); + + this.lastResponse = txHash; + } catch (e) { + console.log(e); + } + }, + async switchChain(chainId) { + console.debug(`switching to network chainId=${chainId}`); + try { + const response = await this.provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: chainId }], // chainId must be in hexadecimal numbers + }); + console.debug(`response`, response); } catch (err) { console.error(err); } }, + async readOnlyCalls() { + if(!this.sdk?.hasReadOnlyRPCCalls() && !this.provider){ + this.lastResponse('readOnlyCalls are not set and provider is not set. Please set your infuraAPIKey in the SDK Options'); + return; + } + try { + const result = await this.provider.request({ + method: 'eth_blockNumber', + params: [], + }); + const gotFrom = this.sdk.hasReadOnlyRPCCalls() ? 'infura' : 'MetaMask provider'; + this.lastResponse = `(${gotFrom}) ${result}`; + } catch (e) { + console.log(`error getting the blockNumber`, e); + this.lastResponse = 'error getting the blockNumber'; + } + }, + async changeLanguage() { + localStorage.setItem('MetaMaskSDKLng', this.selectedLanguage); + window.location.reload(); + }, terminate() { this.sdk?.terminate(); - this.accounts = null; + this.account = null; this.lastResponse = "Terminated!"; this.chainId = null; } @@ -159,43 +308,56 @@ export default {