From 304cd457c052e857bb6d2d5f5305baec00f43219 Mon Sep 17 00:00:00 2001 From: Julien Eluard Date: Thu, 25 Jan 2024 16:56:52 +0100 Subject: [PATCH] feat: add ERC-55 support to ExecutionAddress --- packages/types/package.json | 3 ++- packages/types/src/primitive/sszTypes.ts | 15 ++++++++++++++- packages/utils/package.json | 1 + packages/utils/src/format.ts | 24 +++++++++++++++++++++++- packages/utils/test/unit/format.test.ts | 19 +++++++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 packages/utils/test/unit/format.test.ts diff --git a/packages/types/package.json b/packages/types/package.json index 4497098c14f4..ce7265bdf211 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -74,7 +74,8 @@ "types": "lib/index.d.ts", "dependencies": { "@chainsafe/ssz": "^0.14.0", - "@lodestar/params": "^1.14.0" + "@lodestar/params": "^1.14.0", + "@lodestar/utils": "^1.14.0" }, "keywords": [ "ethereum", diff --git a/packages/types/src/primitive/sszTypes.ts b/packages/types/src/primitive/sszTypes.ts index 65c81d1247b9..92b67a79e89e 100644 --- a/packages/types/src/primitive/sszTypes.ts +++ b/packages/types/src/primitive/sszTypes.ts @@ -1,4 +1,7 @@ import {ByteVectorType, UintNumberType, UintBigintType, BooleanType} from "@chainsafe/ssz"; +/* eslint-disable no-restricted-imports */ +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 +57,16 @@ 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 { + const string = super.toJson(value) as string; + return toChecksumAddress(string); + } +} + export const Version = Bytes4; export const DomainType = Bytes4; export const ForkDigest = Bytes4; @@ -61,4 +74,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/package.json b/packages/utils/package.json index db3ed9e06986..67bd6948be92 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -44,6 +44,7 @@ "bigint-buffer": "^1.1.5", "case": "^1.6.3", "chalk": "^5.2.0", + "ethereum-cryptography": "^1.2.0", "js-yaml": "^4.1.0" }, "devDependencies": { diff --git a/packages/utils/src/format.ts b/packages/utils/src/format.ts index 6a88ead41490..3c9cf513b5ee 100644 --- a/packages/utils/src/format.ts +++ b/packages/utils/src/format.ts @@ -1,4 +1,5 @@ -import {toHexString} from "./bytes.js"; +import {keccak256} from "ethereum-cryptography/keccak.js"; +import {fromHex, toHexString} from "./bytes.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; +} diff --git a/packages/utils/test/unit/format.test.ts b/packages/utils/test/unit/format.test.ts new file mode 100644 index 000000000000..524c0b570dba --- /dev/null +++ b/packages/utils/test/unit/format.test.ts @@ -0,0 +1,19 @@ +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" + ); + }); +});