-
Notifications
You must be signed in to change notification settings - Fork 103
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
wip on preminter test wip on scanning for the event; currently we cannot deploy the contract cause its too large :( added test that shows how to recover the signer
- Loading branch information
Showing
4 changed files
with
458 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
import { AbiParameter } from "abitype"; | ||
import { | ||
Address, | ||
HashTypedDataParameters, | ||
Hex, | ||
TypedData, | ||
TypedDataDomain, | ||
TypedDataParameter, | ||
concat, | ||
encodeAbiParameters, | ||
keccak256, | ||
toHex, | ||
} from "viem"; | ||
|
||
export type MessageTypeProperty = { | ||
name: string; | ||
type: string; | ||
}; | ||
|
||
export function hashedTypedDataV4< | ||
const TTypedData extends TypedData | { [key: string]: unknown }, | ||
TPrimaryType extends string = string | ||
>({ | ||
structHash, | ||
chainId, | ||
verifyingContract, | ||
_types, | ||
name, | ||
version, | ||
}: { | ||
structHash: `0x${string}`; | ||
verifyingContract: Address; | ||
chainId: number; | ||
_types: HashTypedDataParameters<TTypedData, TPrimaryType>["types"]; | ||
name: string; | ||
version: string; | ||
}) { | ||
const domain: TypedDataDomain = { | ||
chainId, | ||
name, | ||
version, | ||
verifyingContract, | ||
}; | ||
|
||
const types = { | ||
EIP712Domain: getTypesForEIP712Domain({ domain }), | ||
...(_types as TTypedData), | ||
}; | ||
|
||
const parts: Hex[] = ["0x1901"]; | ||
|
||
// hash the eip 712 domain, verifying contract, and chain id. | ||
parts.push( | ||
hashDomain({ | ||
domain, | ||
types: types as Record<string, MessageTypeProperty[]>, | ||
}) | ||
); | ||
|
||
// append the already hashed message | ||
parts.push(structHash); | ||
|
||
return keccak256(concat(parts)); | ||
} | ||
|
||
export function hashDomain({ | ||
domain, | ||
types, | ||
}: { | ||
domain: TypedDataDomain; | ||
types: Record<string, MessageTypeProperty[]>; | ||
}) { | ||
return hashStruct({ | ||
data: domain, | ||
primaryType: "EIP712Domain", | ||
types, | ||
}); | ||
} | ||
|
||
function hashStruct({ | ||
data, | ||
primaryType, | ||
types, | ||
}: { | ||
data: Record<string, unknown>; | ||
primaryType: string; | ||
types: Record<string, MessageTypeProperty[]>; | ||
}) { | ||
const encoded = encodeData({ | ||
data, | ||
primaryType, | ||
types, | ||
}); | ||
return keccak256(encoded); | ||
} | ||
|
||
function encodeData({ | ||
data, | ||
primaryType, | ||
types, | ||
}: { | ||
data: Record<string, unknown>; | ||
primaryType: string; | ||
types: Record<string, MessageTypeProperty[]>; | ||
}) { | ||
const encodedTypes: AbiParameter[] = [{ type: "bytes32" }]; | ||
const encodedValues: unknown[] = [hashType({ primaryType, types })]; | ||
|
||
for (const field of types[primaryType]!) { | ||
const [type, value] = encodeField({ | ||
types, | ||
name: field.name, | ||
type: field.type, | ||
value: data[field.name], | ||
}); | ||
encodedTypes.push(type); | ||
encodedValues.push(value); | ||
} | ||
|
||
return encodeAbiParameters(encodedTypes, encodedValues); | ||
} | ||
|
||
function hashType({ | ||
primaryType, | ||
types, | ||
}: { | ||
primaryType: string; | ||
types: Record<string, MessageTypeProperty[]>; | ||
}) { | ||
const encodedHashType = toHex(encodeType({ primaryType, types })); | ||
return keccak256(encodedHashType); | ||
} | ||
|
||
function encodeType({ | ||
primaryType, | ||
types, | ||
}: { | ||
primaryType: string; | ||
types: Record<string, MessageTypeProperty[]>; | ||
}) { | ||
let result = ""; | ||
const unsortedDeps = findTypeDependencies({ primaryType, types }); | ||
unsortedDeps.delete(primaryType); | ||
|
||
const deps = [primaryType, ...Array.from(unsortedDeps).sort()]; | ||
for (const type of deps) { | ||
result += `${type}(${types[type]!.map( | ||
({ name, type: t }) => `${t} ${name}` | ||
).join(",")})`; | ||
} | ||
|
||
return result; | ||
} | ||
|
||
function findTypeDependencies( | ||
{ | ||
primaryType: primaryType_, | ||
types, | ||
}: { | ||
primaryType: string; | ||
types: Record<string, MessageTypeProperty[]>; | ||
}, | ||
results: Set<string> = new Set() | ||
): Set<string> { | ||
const match = primaryType_.match(/^\w*/u); | ||
const primaryType = match?.[0]!; | ||
if (results.has(primaryType) || types[primaryType] === undefined) { | ||
return results; | ||
} | ||
|
||
results.add(primaryType); | ||
|
||
for (const field of types[primaryType]!) { | ||
findTypeDependencies({ primaryType: field.type, types }, results); | ||
} | ||
return results; | ||
} | ||
|
||
function encodeField({ | ||
types, | ||
name, | ||
type, | ||
value, | ||
}: { | ||
types: Record<string, MessageTypeProperty[]>; | ||
name: string; | ||
type: string; | ||
value: any; | ||
}): [type: AbiParameter, value: any] { | ||
if (types[type] !== undefined) { | ||
return [ | ||
{ type: "bytes32" }, | ||
keccak256(encodeData({ data: value, primaryType: type, types })), | ||
]; | ||
} | ||
|
||
if (type === "bytes") { | ||
const prepend = value.length % 2 ? "0" : ""; | ||
value = `0x${prepend + value.slice(2)}`; | ||
return [{ type: "bytes32" }, keccak256(value)]; | ||
} | ||
|
||
if (type === "string") return [{ type: "bytes32" }, keccak256(toHex(value))]; | ||
|
||
if (type.lastIndexOf("]") === type.length - 1) { | ||
const parsedType = type.slice(0, type.lastIndexOf("[")); | ||
const typeValuePairs = (value as [AbiParameter, any][]).map((item) => | ||
encodeField({ | ||
name, | ||
type: parsedType, | ||
types, | ||
value: item, | ||
}) | ||
); | ||
return [ | ||
{ type: "bytes32" }, | ||
keccak256( | ||
encodeAbiParameters( | ||
typeValuePairs.map(([t]) => t), | ||
typeValuePairs.map(([, v]) => v) | ||
) | ||
), | ||
]; | ||
} | ||
|
||
return [{ type }, value]; | ||
} | ||
|
||
export function getTypesForEIP712Domain({ | ||
domain, | ||
}: { | ||
domain?: TypedDataDomain; | ||
}): TypedDataParameter[] { | ||
return [ | ||
typeof domain?.name === "string" && { name: "name", type: "string" }, | ||
domain?.version && { name: "version", type: "string" }, | ||
typeof domain?.chainId === "number" && { | ||
name: "chainId", | ||
type: "uint256", | ||
}, | ||
domain?.verifyingContract && { | ||
name: "verifyingContract", | ||
type: "address", | ||
}, | ||
domain?.salt && { name: "salt", type: "bytes32" }, | ||
].filter(Boolean) as TypedDataParameter[]; | ||
} |
Oops, something went wrong.