diff --git a/src/coin/eth.test.ts b/src/coin/eth.test.ts index 86260c80..b924e346 100644 --- a/src/coin/eth.test.ts +++ b/src/coin/eth.test.ts @@ -2,15 +2,32 @@ import { hexToBytes } from "@noble/hashes/utils"; import { describe, expect, test } from "bun:test"; import { decodeEthAddress, encodeEthAddress } from "./eth.js"; +const prefixlessAddress = "314159265dD8dbb310642f98f50C066173C1259b"; +const hex = "314159265dd8dbb310642f98f50c066173c1259b"; + +test(`eth address: encode 0x${prefixlessAddress}`, () => { + expect(encodeEthAddress(hexToBytes(hex))).toEqual(`0x${prefixlessAddress}`); +}); +test("eth address: invalid checksum", () => { + expect(() => + decodeEthAddress("0x314159265Dd8Dbb310642f98F50C066173C1259b") + ).toThrow(); +}); + describe.each([ { - text: "0x314159265dD8dbb310642f98f50C066173C1259b", - hex: "314159265dd8dbb310642f98f50c066173c1259b", + // checksummed address + text: `0x${prefixlessAddress}`, }, -])("eth address", ({ text, hex }) => { - test(`encode: ${text}`, () => { - expect(encodeEthAddress(hexToBytes(hex))).toEqual(text); - }); + { + // all lowercased address + text: `0x${prefixlessAddress.toLowerCase()}`, + }, + { + // all uppercased address + text: `0x${prefixlessAddress.toUpperCase()}`, + }, +])("eth address", ({ text }) => { test(`decode: ${text}`, () => { expect(decodeEthAddress(text)).toEqual(hexToBytes(hex)); }); diff --git a/src/utils/hex.test.ts b/src/utils/hex.test.ts new file mode 100644 index 00000000..a4ea94a6 --- /dev/null +++ b/src/utils/hex.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "bun:test"; +import { isValidChecksumAddress } from "./hex.js"; + +const prefixlessAddress = "314159265dD8dbb310642f98f50C066173C1259b"; + +describe("isValidChecksumAddress()", () => { + test("valid checksum address", () => { + expect(isValidChecksumAddress(`0x${prefixlessAddress}`)).toBeTrue(); + }); + test("all lowercased address", () => { + expect( + isValidChecksumAddress(`0x${prefixlessAddress.toLowerCase()}`) + ).toBeTrue(); + }); + test("all uppercased address", () => { + expect( + isValidChecksumAddress(`0x${prefixlessAddress.toUpperCase()}`) + ).toBeTrue(); + }); + test("invalid checksum address", () => { + expect( + isValidChecksumAddress("0x314159265Dd8Dbb310642f98F50C066173C1259b") + ).toBeFalse(); + }); + test("non-hex", () => { + expect( + isValidChecksumAddress("0x1234567890abcdefghijklmnopqrstuvwxyz0123") + ).toBeFalse(); + }); +}); diff --git a/src/utils/hex.ts b/src/utils/hex.ts index 8104face..e3f6731b 100644 --- a/src/utils/hex.ts +++ b/src/utils/hex.ts @@ -45,7 +45,17 @@ export function isValidChecksumAddress( address: string, chainId?: number ): boolean { - return isAddress(address) && checksumAddress(address, chainId) === address; + if (!isAddress(address)) return false; + + if (address === checksumAddress(address, chainId)) return true; + + const prefixlessAddress = stripHexPrefix(address); + // all lowercase + if (prefixlessAddress.toLowerCase() === prefixlessAddress) return true; + // all uppercase + if (prefixlessAddress.toUpperCase() === prefixlessAddress) return true; + + return false; } export const createHexChecksummedEncoder = diff --git a/src/utils/index.ts b/src/utils/index.ts index 7122e3dd..52781053 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -44,6 +44,15 @@ export { isEvmCoinType, } from "./evm.js"; export { validateFlowAddress } from "./flow.js"; +export { + checksumAddress, + createHexChecksummedDecoder, + createHexChecksummedEncoder, + isAddress, + isValidChecksumAddress, + rawChecksumAddress, + stripHexPrefix, +} from "./hex.js"; export { decodeLeb128, encodeLeb128 } from "./leb128.js"; export { validateNearAddress } from "./near.js"; export { createZcashDecoder, createZcashEncoder } from "./zcash.js";