Files
openzeppelin-contracts/test/helpers/erc7739.js
2025-05-06 12:47:36 -06:00

119 lines
3.3 KiB
JavaScript

const { ethers } = require('hardhat');
const { formatType } = require('./eip712');
const PersonalSign = formatType({ prefixed: 'bytes' });
const TypedDataSign = contentsTypeName =>
formatType({
contents: contentsTypeName,
name: 'string',
version: 'string',
chainId: 'uint256',
verifyingContract: 'address',
salt: 'bytes32',
});
class ERC7739Signer extends ethers.AbstractSigner {
#signer;
#domain;
constructor(signer, domain) {
super(signer.provider);
this.#signer = signer;
this.#domain = domain;
}
static from(signer, domain) {
return new this(signer, domain);
}
get signingKey() {
return this.#signer.signingKey;
}
get privateKey() {
return this.#signer.privateKey;
}
async getAddress() {
return this.#signer.getAddress();
}
connect(provider) {
this.#signer.connect(provider);
}
async signTransaction(tx) {
return this.#signer.signTransaction(tx);
}
async signMessage(message) {
return this.#signer.signTypedData(this.#domain, { PersonalSign }, ERC4337Utils.preparePersonalSign(message));
}
async signTypedData(domain, types, value) {
const { allTypes, contentsTypeName, contentsDescr } = ERC4337Utils.getContentsDetail(types);
return Promise.resolve(
this.#signer.signTypedData(domain, allTypes, ERC4337Utils.prepareSignTypedData(value, this.#domain)),
).then(signature =>
ethers.concat([
signature,
ethers.TypedDataEncoder.hashDomain(domain), // appDomainSeparator
ethers.TypedDataEncoder.hashStruct(contentsTypeName, types, value), // contentsHash
ethers.toUtf8Bytes(contentsDescr),
ethers.toBeHex(contentsDescr.length, 2),
]),
);
}
}
class ERC4337Utils {
static preparePersonalSign(message) {
return {
prefixed: ethers.concat([
ethers.toUtf8Bytes(ethers.MessagePrefix),
ethers.toUtf8Bytes(String(message.length)),
typeof message === 'string' ? ethers.toUtf8Bytes(message) : message,
]),
};
}
static prepareSignTypedData(contents, signerDomain) {
return {
name: signerDomain.name ?? '',
version: signerDomain.version ?? '',
chainId: signerDomain.chainId ?? 0,
verifyingContract: signerDomain.verifyingContract ?? ethers.ZeroAddress,
salt: signerDomain.salt ?? ethers.ZeroHash,
contents,
};
}
static getContentsDetail(contentsTypes, contentsTypeName = Object.keys(contentsTypes).at(0)) {
// Examples values
//
// contentsTypeName B
// typedDataSignType TypedDataSign(B contents,...)A(uint256 v)B(Z z)Z(A a)
// contentsType A(uint256 v)B(Z z)Z(A a)
// contentsDescr A(uint256 v)B(Z z)Z(A a)B
const allTypes = { TypedDataSign: TypedDataSign(contentsTypeName), ...contentsTypes };
const typedDataSignType = ethers.TypedDataEncoder.from(allTypes).encodeType('TypedDataSign');
const contentsType = typedDataSignType.slice(typedDataSignType.indexOf(')') + 1); // Remove TypedDataSign (first object)
const contentsDescr = contentsType + (contentsType.startsWith(contentsTypeName) ? '' : contentsTypeName);
return {
allTypes,
contentsTypes,
contentsTypeName,
contentsDescr,
};
}
}
module.exports = {
ERC7739Signer,
ERC4337Utils,
PersonalSign,
TypedDataSign,
};