Signing messages
Immutable Passport supports signing the following message standards:
Sign Messages
- EIP-712
- ERC-191
Create the Web3Provider by fetching the Passport provider by calling passportInstance.connectEvm()
after initialising Passport.
Then create a new EtherJS Web3Provider
and pass the Passport provider into the constructor.
// fetch the Passport provider from the Passport instance
const passportProvider = passportInstance.connectEvm();
// create the Web3Provider using the Passport provider
const web3Provider = new ethers.providers.Web3Provider(passportProvider);
Once the Web3Provider is created, call eth_requestAccounts
to trigger the login flow for Passport if the user is not signed in.
// calling eth_requestAccounts triggers the Passport login flow
const accounts = await web3Provider.provider.request({ method: 'eth_requestAccounts' });
Retrieve the signer from the Web3Provider
, set the chainId (zkEVM Testnet) and use our utility function to
get a typed payload which contains our domain, message and types (see the utils directory for the full details). Then call
'eth_signTypedData_v4',
to trigger the Passport popup for the user to sign the message.
const signMessage = async () => {
// set signed state message to pending in the view
setSignedMessageState('pending signature');
// fetch the signer from the Web3provider
const signer = web3Provider.getSigner();
// set the chainId
const chainId = 13473; // zkEVM testnet
// set the sender address
const address = await signer.getAddress();
// get our message payload - including domain, message and types (see utils/etherMailTypedPayload)
const etherMailTypedPayload = getEtherMailTypedPayload(chainId, address)
setParams([
address,
etherMailTypedPayload
])
try {
// attempt to sign the message, this brings up the passport popup
// if successful update the signed message to successful in the view
const signature = await passportProvider.request({
method: 'eth_signTypedData_v4',
params: [address, etherMailTypedPayload],
})
setSignature(signature)
setSignedMessageState('user successfully signed message');
} catch (error: any) {
// Handle user denying signature
if (error.code === 4001) {
// if the user declined update the signed message to declined in the view
setSignedMessageState('user declined to sign');
} else {
// if something else went wrong, update the generic error with message in the view
setSignedMessageState(`something went wrong - ${error.message}`);
console.log(error);
}
}
};
It's important to note the chainId
must match the chainId your passport instance is connected to, in this case it's zkEVM testnet.
You should also handle the errors returned by the Passport popup by wrapping the await in try catch statement and notifying the user if there is an error.
Checkout the full working example here: Passport with NextJS Singing with EIP-712.
Create the Web3Provider by fetching the Passport provider by calling passportInstance.connectEvm()
after initialising Passport.
Then create a new EtherJS Web3Provider
and pass the Passport provider into the constructor.
// fetch the Passport provider from the Passport instance
const passportProvider = passportInstance.connectEvm();
// create the Web3Provider using the Passport provider
const web3Provider = new ethers.providers.Web3Provider(passportProvider);
Once the Web3Provider is created, call eth_requestAccounts
to trigger the login flow for Passport if the user is not signed in.
// calling eth_requestAccounts triggers the Passport login flow
const accounts = await web3Provider.provider.request({ method: 'eth_requestAccounts' });
Retrieve the signer from the Web3Provider
, create the message and call signer.signMessage(message)
to
trigger the Passport popup for the user to sign the message.
const signMessage = async () => {
// set signed state message to pending in the view
setSignedMessageState('pending signature');
// fetch the signer from the Web3provider
const signer = web3Provider.getSigner();
const address = await signer.getAddress();
setAddress(address);
// Create the message to be signed
// Please note there is a 500 character limit for the message
const message = 'this is a personal sign message';
setPersonalMessage(message);
try {
// attempt to sign the message, this brings up the passport popup
const signature = await signer.signMessage(message);
setSignature(signature);
// if successful update the signed message to successful in the view
setSignedMessageState('user successfully signed message');
} catch (error: any) {
// Handle user denying signature
if (error.code === -32003) {
// if the user declined update the signed message to declined in the view
setSignedMessageState('user declined to sign');
} else {
// if something else went wrong, update the generic error with message in the view
setSignedMessageState(`something went wrong - ${error.message}`);
console.log(error)
}
}
};
It's important to note this type of message has a 500 character limit.
You should also handle the errors returned by the Passport popup by wrapping the await in try catch statement and notifying the user if there is an error.
Checkout the full working example here: Passport with NextJS Singing with ERC-191.
Verify Signatures
Verifying the authenticity of a signature can be done by calling the respective methods which wrap isValidSignature
. Check the utils directory for the implementation.
- Verify EIP-712 signature
- Verify ERC-191 signature
Here is the wrapper code from the example app showing signature verfication for EIP712s.
const isValidTypedDataSignature = async (
address: string, //The Passport wallet address returned from eth_requestAccounts
payload: string, //The stringified payload
signature: string, //The signature
zkEvmProvider: passport.Provider, // can be any provider, Passport or not
) => {
const typedPayload: passport.TypedDataPayload = JSON.parse(payload);
const types = { ...typedPayload.types };
// @ts-ignore
// Ethers auto-generates the EIP712Domain type in the TypedDataEncoder, and so it needs to be removed
delete types.EIP712Domain;
//The hashed string
const digest = ethers.utils._TypedDataEncoder.hash(
typedPayload.domain,
types,
typedPayload.message,
);
return isValidSignature(address, digest, signature, zkEvmProvider);
};
const verifySignature = async () => {
setVerifiedStateMessage("Pending Verification");
try {
// validate the signature
const isValid = await isValidTypedDataSignature(
params[0], // the signer address
JSON.stringify(params[1]), // the etherMail payload
signature,
passportProvider,
);
// set verified message state based on validation value
isValid ? setVerifiedStateMessage("Signature verified") : setVerifiedStateMessage("Signature couldn't be verified");
} catch (error: any) {
// if something else went wrong, update the generic error with message in the view
setSignedMessageState(`something went wrong - ${error.message}`);
console.log(error);
}
}
Here is the wrapper code from the example app showing signature verfication for ERC191s.
const isValidERC191Signature = async (
address: string, // The wallet address returned from eth_requestAccounts
payload: string, // The message string
signature: string, // The signature
zkEvmProvider: passport.Provider, // Can be any provider, Passport or not
) => {
const digest = utils.hashMessage(payload);
return isValidSignature(address, digest, signature, zkEvmProvider);
};
const verifySignature = async () => {
setVerifiedStateMessage("Pending Verification");
try {
// validate the signature
const isValid = await isValidERC191Signature(address, personalMessage, msgSignature, passportProvider);
isValid ? setVerifiedStateMessage("Signature verified") : setVerifiedStateMessage("Signature couldn't be verified");
} catch (error: any) {
// if something else went wrong, update the generic error with message in the view
setVerifiedStateMessage(`something went wrong - ${error.message}`);
console.log(error);
}
}