constsig=awaitwindow.ethereum.request({ method:'personal_sign',//May be eth_sign on some wallets params: [address,payload.jsonToSign]})
EIP-712
Important: EIP-712 is limited in message size. Due to its poor performance, it greatly slows down the blockchain, so we only allow it to be used for small messages.
To sign with Metamask, replace the signature logic in the snipper above with the code below.
//Add necessary importsimport { _TypedDataEncoder } from'ethers/lib/utils.js';// Init Metamaskawaitwindow.ethereum.enable()constethAddress='...'//Option 1: window.ethereumconsteip=_TypedDataEncoder.getPayload(payload.eipToSign.domain, types_,payload.eipToSign.message);constsig=awaitwindow.ethereum.request({ method:'eth_signTypedData_v4', params: [ethAddress,JSON.stringify(eip)],})//Option 2: Use the signer._signTypedData from ethers directly (https://docs.ethers.org/v5/api/signer/#Signer-signTypedData)
//It will give an error with an unused EIP712Domain type, so you have to remove that before calling it as seen below.//It adds this type automatically.//From https://github.com/wagmi-dev/wagmi/blob/main/packages/core/src/actions/accounts/signTypedData.ts#L41consttypes_=Object.entries(payload.eipToSign.types).filter(([key]) => key !=='EIP712Domain').reduce((types, [key, attributes]: [string,TypedDataField[]]) => { types[key] =attributes.filter((attr) =>attr.type !=='EIP712Domain')return types }, {} asRecord<string,TypedDataField[]>)// Method name may be changed in the future, see https://docs.ethers.io/v5/api/signer/#Signer-signTypedDatareturnawaitsigner._signTypedData(payload.eipToSign.domain, types_,payload.eipToSign.message)
Output
This will leave you with a variable which is to be submitted to a running blockchain node. See Broadcast to a Node.
import { Secp256k1 } from'@cosmjs/crypto';import { disconnect as disconnectWeb3, signMessage, signTypedData } from'@wagmi/core';import { useWeb3Modal } from'@web3modal/wagmi/react';import { notification } from'antd';import { TransactionPayload, TxContext, convertToCosmosAddress, createTxBroadcastBody } from'bitbadgesjs-sdk';import { ethers } from'ethers';import { createContext, useContext } from'react';import { useCookies } from'react-cookie';import { useAccount as useWeb3Account } from'wagmi';import { ChainSpecificContextType } from'../ChainContext';import { setPublicKey, useAccount } from'../accounts/AccountsContext';import { ChainDefaultState } from'./helpers';exporttypeEthereumContextType=ChainSpecificContextType;exportconstEthereumContext=createContext<EthereumContextType>({ ...ChainDefaultState });interfaceProps { children?:React.ReactNode;}exportconstEthereumContextProvider:React.FC<Props> = ({ children }) => {const [cookies,setCookies] =useCookies(['blockincookie','pub_key']);const { open } =useWeb3Modal();constweb3AccountContext=useWeb3Account();constaddress=web3AccountContext.address ||'';constcosmosAddress=convertToCosmosAddress(address);constaccount=useAccount(cosmosAddress);constautoConnect=async () => {};constconnect=async () => {if (!address) {try {awaitopen(); } catch (e) {notification.error({ message:'Error connecting to wallet', description: 'Make sure you have a compatible Ethereum wallet installed (such as MetaMask) and that you are signed in to it.'
}); } } };constdisconnect=async () => {awaitdisconnectWeb3(); };constsignChallenge=async (message:string) => {constsign=awaitsignMessage({ message: message });constmsgHash=ethers.utils.hashMessage(message);constmsgHashBytes=ethers.utils.arrayify(msgHash);constpubKey=ethers.utils.recoverPublicKey(msgHashBytes, sign);constpubKeyHex=pubKey.substring(2);constcompressedPublicKey=Secp256k1.compressPubkey(newUint8Array(Buffer.from(pubKeyHex,'hex')));constbase64PubKey=Buffer.from(compressedPublicKey).toString('base64');setPublicKey(cosmosAddress, base64PubKey);setCookies('pub_key',`${cosmosAddress}-${base64PubKey}`, { path:'/' });return { message, signature: sign }; };constsignTxn=async (context:TxContext, payload:TransactionPayload, simulate:boolean) => {if (!account) thrownewError('Account not found.');//If we are within ~1000 chars limit, we can have user sign the typed EIP712//Else, we hash the JSON and have user sign the hashconstnormalMessage=payload.jsonToSign.length<1000;let sig ='';if (normalMessage) {if (!simulate) { sig =awaitsignTypedData({ message:payload.eipToSign.message asany, types:payload.eipToSign.types asany, domain:payload.eipToSign.domain, primaryType:payload.eipToSign.primaryType }); } } else {if (!simulate) {notification.warn({ message:'Alternative Method', description: `This transaction message is very large, so we must resort to an alternative method of signing (JSON).
The transaction may be displayed in a different format than you are used to.` }); const message = payload.jsonToSign; sig = await signMessage({ message: message }); } } const txBody = createTxBroadcastBody(context, payload, sig); return txBody; }; const getPublicKey = async (_cosmosAddress: string) => { try { const currAccount = account; //If we have stored the public key in cookies, use that instead (for Ethereum) if (currAccount && cookies.pub_key && cookies.pub_key.split('-')[0] === currAccount.cosmosAddress) { return cookies.pub_key.split('-')[1]; } if (currAccount?.publicKey) { return currAccount.publicKey; } const message = "Hello! We noticed that you haven't interacted the BitBadges blockchain yet. To interact with the BitBadges blockchain, we need your PUBLIC key for your address to allow us to generate transactions.\n\nPlease kindly sign this message to allow us to compute your public key.\n\nThis message is not a blockchain transaction and signing this message has no purpose other than to compute your public key.\n\nThanks for your understanding!";
const sig = await signMessage({ message: message }); const msgHash = ethers.utils.hashMessage(message); const msgHashBytes = ethers.utils.arrayify(msgHash); const pubKey = ethers.utils.recoverPublicKey(msgHashBytes, sig); const pubKeyHex = pubKey.substring(2); const compressedPublicKey = Secp256k1.compressPubkey(new Uint8Array(Buffer.from(pubKeyHex, 'hex'))); const base64PubKey = Buffer.from(compressedPublicKey).toString('base64'); setPublicKey(_cosmosAddress, base64PubKey); setCookies('pub_key', `${_cosmosAddress}-${base64PubKey}`, { path: '/' }); return base64PubKey; } catch (e) { console.log(e); return ''; } }; const ethereumContext: EthereumContextType = { autoConnect, connect, disconnect, signChallenge, signTxn, address, getPublicKey }; return <EthereumContext.Provider value={ethereumContext}>{children}</EthereumContext.Provider>;};export const useEthereumContext = () => useContext(EthereumContext);