On my frontend app I am trying to implement EIP-3009 with USDC using Ethers v6.13.4, Web3Auth (using Web3AuthNoModal) and Alchemy as the RPC provider. The implementation itself seems to be very straightforward, here is the code:
import { WalletContext } from "../contexts/wallet"; ... const { provider, web3auth } = useContext(WalletContext); ... const ethersProvider = new ethers.BrowserProvider(provider as any); const web3AuthSigner = await ethersProvider.getSigner(); const address = await web3AuthSigner.getAddress(); const amountToTransfer = ethers.parseUnits(amount.toString(), 6); // Generate deadline (1 hour) const now = new Date(); const deadline = new Date(now.getTime() + 10 * 60 * 1000); // Generate a random nonce const randomBytes = ethers.randomBytes(32); const nonce = ethers.hexlify(randomBytes); const chainId = parseInt(process.env.CHAIN_ID, 16); // ABI for the authorizationState function in the USDC contract const usdcContract = new ethers.Contract( erc20Address, [ { inputs: [ { internalType: "address", name: "authorizer", type: "address" }, { internalType: "bytes32", name: "nonce", type: "bytes32" }, ], name: "authorizationState", outputs: [{ internalType: "bool", name: "", type: "bool" }], stateMutability: "view", type: "function", }, ], web3AuthSigner ); // Check if the nonce has already been used const alreadyAuthorized = await usdcContract.authorizationState(address, nonce); if (alreadyAuthorized) { throw new Error("Nonce has already been used."); } // Define the domain and message types for the signed data console.info(process.env.CHAIN_ID); console.info(parseInt(process.env.CHAIN_ID)); const domain = { name: "USDC", version: "2", chainId: parseInt(process.env.CHAIN_ID), verifyingContract: erc20Address, }; const types = { TransferWithAuthorization: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, ], }; // Message to sign (EIP-3009 structure) const message = { from: address, to: toAddress, value: amountToTransfer, validAfter: 0, validBefore: deadline.valueOf(), nonce: nonce, }; console.info({ message }); let signature; try { // Sign the typed data with the user's wallet signature = await web3AuthSigner.signTypedData(domain, types, message); console.info("Signature: ", signature); } catch (error) { console.error("Error signing the authorization:", error); throw error; } I've copied this implementation from a number of different projects, however, in this project, it always fails despite the same version of Ethers, using Web3Auth and even using Alchemy as the RPC provider. Here is the error message when applying it on Ethereum Sepolia (I've omitted the addresses used, but I can assure that they are always the expected addresses):
could not coalesce error (error={ "code": -32603, "message": "Cannot read properties of null (reading 'ok')" }, payload={ "id": 6, "jsonrpc": "2.0", "method": "eth_signTypedData_v4", "params": [ "0x...", "{\"types\":{\"TransferWithAuthorization\":[{\"name\":\"from\",\"type\":\"address\"},{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"validAfter\",\"type\":\"uint256\"},{\"name\":\"validBefore\",\"type\":\"uint256\"},{\"name\":\"nonce\",\"type\":\"bytes32\"}],\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}]},\"domain\":{\"name\":\"USDC\",\"version\":\"2\",\"chainId\":\"0xaa36a7\",\"verifyingContract\":\"0x1c7d4b196cb0c7b01d743fbc6116a902379c7238\"},\"primaryType\":\"TransferWithAuthorization\",\"message\":{\"from\":\"0x...\",\"to\":\"0x...\",\"value\":\"100000\",\"validAfter\":\"0\",\"validBefore\":\"1734784620899\",\"nonce\":\"0xa27be55ed87a2b4a2e38db6d4864816f76d45dc27b9cae4b2b0415ef1ef2523e\"}}" ] }, code=UNKNOWN_ERROR, version=6.13.4) This error only happens at the end of the script, while using signTypedData(domain, types, message), which makes an error with code 32603 even stranger, as I previously check on-chain if the nonce has already been used or not. I've tried to change the keys to the provider but to no avail.
Any lights on the problem would be greatly appreciated. Thank you in advance!
P.S.:
Since the issue is most likely on the way I create the provider, I'll share the way I instantiate it. Since I am using React, I am only sharing the relevant bits for this context. These snippets are not all contained within the same function. Regarding the React logic itself, the only thing relevant is that I have useState for the variable provider that I will add to a context script that allows me to use it throughout the whole app.
I am using Web3AuthNoModal and EthereumPrivateKeyProvider in the following way on a different script:
const [provider, setProvider] = useState<IProvider | null>(null); ... const chainConfig = { chainNamespace: CHAIN_NAMESPACES.EIP155, chainId: process.env.CHAIN_ID, rpcTarget: process.env.ALCHEMY_RPC_TARGET, }; const privateKeyProvider = new EthereumPrivateKeyProvider({ config: { chainConfig }, }); ... const web3auth = useMemo( () => new Web3AuthNoModal({ clientId: process.env.WEB3AUTH_CLIENT_ID, web3AuthNetwork: process.env.WEB3AUTH_NETWORK, privateKeyProvider: privateKeyProvider, }), [] ); ... web3auth.configureAdapter(openloginAdapter); await web3auth.init(); setProvider(web3auth.provider); ... return ( ... <WalletContext.Provider value={{ provider, setProvider, ... }} > ... </WalletContext.Provider> Here is the context script for the wallet:
export interface IContextValues { provider: IProvider | null; setProvider: Dispatch<SetStateAction<IProvider | null>>; ... } export const WalletContext = createContext<IContextValues>({ provider: null, setProvider: () => null, ... }); Finally, the way I import this context and use it is on the first piece of code that I've shared.