diff --git a/packages/types/src/primitive/sszTypes.ts b/packages/types/src/primitive/sszTypes.ts index 65c81d1247b9..328fc5b889e4 100644 --- a/packages/types/src/primitive/sszTypes.ts +++ b/packages/types/src/primitive/sszTypes.ts @@ -1,4 +1,6 @@ import {ByteVectorType, UintNumberType, UintBigintType, BooleanType} from "@chainsafe/ssz"; +import {ByteArray} from "@chainsafe/ssz/lib/type/byteArray"; +import {toChecksumAddress} from "@lodestar/utils"; export const Boolean = new BooleanType(); export const Byte = new UintNumberType(1); @@ -54,6 +56,15 @@ export const Wei = UintBn256; export const Root = new ByteVectorType(32); export const BlobIndex = UintNum64; +export class ExecutionAddressType extends ByteVectorType { + constructor() { + super(20); + } + toJson(value: ByteArray): unknown { + return toChecksumAddress(super.toJson(value) as string); + } +}; + export const Version = Bytes4; export const DomainType = Bytes4; export const ForkDigest = Bytes4; @@ -61,4 +72,4 @@ export const BLSPubkey = Bytes48; export const BLSSignature = Bytes96; export const Domain = Bytes32; export const ParticipationFlags = new UintNumberType(1, {setBitwiseOR: true}); -export const ExecutionAddress = Bytes20; +export const ExecutionAddress = new ExecutionAddressType(); diff --git a/packages/utils/src/format.ts b/packages/utils/src/format.ts index 6a88ead41490..3fe96b958b67 100644 --- a/packages/utils/src/format.ts +++ b/packages/utils/src/format.ts @@ -1,4 +1,5 @@ -import {toHexString} from "./bytes.js"; +import {fromHex, toHexString} from "./bytes.js"; +import {keccak256} from "ethereum-cryptography/keccak.js"; /** * Format bytes as `0x1234…1234` @@ -27,3 +28,24 @@ export function truncBytes(root: Uint8Array | string): string { const str = typeof root === "string" ? root : toHexString(root); return str.slice(0, 14); } + +/** + * Formats an address according to [ERC55](https://eips.ethereum.org/EIPS/eip-55) + * + * @param address an hex address + * @returns an ERC55 formatted version of `address` + */ +export function toChecksumAddress(address: string): string { + const rawAddress = address.toLowerCase().startsWith("0x") ? address.slice(2) : address; + const bytes = fromHex(rawAddress); + const hash = toHexString(keccak256(bytes)).slice(2); + let checksumAddress = '0x' + for (let i = 0; i < rawAddress.length; i++) { + if (parseInt(hash[i], 16) >= 8) { + checksumAddress += rawAddress[i].toUpperCase(); + } else { + checksumAddress += rawAddress[i]; + } + } + return checksumAddress; +} \ No newline at end of file diff --git a/packages/utils/test/unit/format.test.ts b/packages/utils/test/unit/format.test.ts new file mode 100644 index 000000000000..99670516028f --- /dev/null +++ b/packages/utils/test/unit/format.test.ts @@ -0,0 +1,11 @@ +import {describe, it, expect} from "vitest"; +import {toChecksumAddress} from "../../src/index.js"; + +describe("toChecksumAddress", () => { + it("should format address as ERC55", () => { + expect(toChecksumAddress("52908400098527886E0F7030069857D2E4169EE7")).toBe("0x52908400098527886E0F7030069857D2E4169EE7"); + expect(toChecksumAddress("0x52908400098527886E0F7030069857D2E4169EE7")).toBe("0x52908400098527886E0F7030069857D2E4169EE7"); + expect(toChecksumAddress("0xde709f2102306220921060314715629080e2fb77")).toBe("0xdE709F2102306220921060314715629080e2fB77"); + expect(toChecksumAddress("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")).toBe("0x5AAEB6053F3E94C9b9A09F33669435E7Ef1BeAED"); + }); +});