diff --git a/CHANGELOG.md b/CHANGELOG.md index d83dce78..8fa3d3d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to ## [Unreleased] +### Added + +- @cosmjs/encoding: Create `normalizeBech32`. + ## [0.28.1] - 2022-03-30 ### Added diff --git a/packages/encoding/src/bech32.spec.ts b/packages/encoding/src/bech32.spec.ts index 39194cfd..1806ddc4 100644 --- a/packages/encoding/src/bech32.spec.ts +++ b/packages/encoding/src/bech32.spec.ts @@ -1,12 +1,12 @@ -import { fromBech32, toBech32 } from "./bech32"; +import { fromBech32, normalizeBech32, toBech32 } from "./bech32"; import { fromHex } from "./hex"; -describe("Bech32", () => { +describe("bech32", () => { // test data generate using https://github.com/nym-zone/bech32 // bech32 -e -h eth 9d4e856e572e442f0a4b2763e72d08a0e99d8ded const ethAddressRaw = fromHex("9d4e856e572e442f0a4b2763e72d08a0e99d8ded"); - describe("encode", () => { + describe("toBech32", () => { it("works", () => { expect(toBech32("eth", ethAddressRaw)).toEqual("eth1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw"); }); @@ -44,7 +44,7 @@ describe("Bech32", () => { }); }); - describe("decode", () => { + describe("fromBech32", () => { it("works", () => { expect(fromBech32("eth1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw")).toEqual({ prefix: "eth", @@ -52,6 +52,15 @@ describe("Bech32", () => { }); }); + it("works for upper case address", () => { + // "For presentation, lowercase is usually preferable, but inside QR codes uppercase SHOULD be used, as those permit the use of alphanumeric mode, which is 45% more compact than the normal byte mode." + // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki + expect(fromBech32("ETH1N48G2MJH9EZZ7ZJTYA37WTGG5R5EMR0DRKWLGW")).toEqual({ + prefix: "eth", + data: ethAddressRaw, + }); + }); + it("works for addresses which exceed the specification limit of 90 characters", () => { // Example from https://github.com/cosmos/cosmos-sdk/pull/6237#issuecomment-658116534 expect(() => @@ -70,5 +79,34 @@ describe("Bech32", () => { ), ).toThrowError(/exceeds length limit/i); }); + + it("throws for mixed case addresses", () => { + // "Decoders MUST NOT accept strings where some characters are uppercase and some are lowercase (such strings are referred to as mixed case strings)." + // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki + expect(() => fromBech32("Eth1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw")).toThrowError(/Mixed-case/i); + expect(() => fromBech32("eTh1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw")).toThrowError(/Mixed-case/i); + expect(() => fromBech32("ETH1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw")).toThrowError(/Mixed-case/i); + expect(() => fromBech32("eth1n48g2mjh9Ezz7zjtya37wtgg5r5emr0drkwlgw")).toThrowError(/Mixed-case/i); + }); + }); + + describe("normalizeBech32", () => { + it("works", () => { + expect(normalizeBech32("eth1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw")).toEqual( + "eth1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw", + ); + expect(normalizeBech32("ETH1N48G2MJH9EZZ7ZJTYA37WTGG5R5EMR0DRKWLGW")).toEqual( + "eth1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw", + ); + }); + + it("throws for mixed case addresses", () => { + // "Decoders MUST NOT accept strings where some characters are uppercase and some are lowercase (such strings are referred to as mixed case strings)." + // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki + expect(() => normalizeBech32("Eth1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw")).toThrowError(/Mixed-case/i); + expect(() => normalizeBech32("eTh1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw")).toThrowError(/Mixed-case/i); + expect(() => normalizeBech32("ETH1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw")).toThrowError(/Mixed-case/i); + expect(() => normalizeBech32("eth1n48g2mjh9Ezz7zjtya37wtgg5r5emr0drkwlgw")).toThrowError(/Mixed-case/i); + }); }); }); diff --git a/packages/encoding/src/bech32.ts b/packages/encoding/src/bech32.ts index 38d4803e..d333611a 100644 --- a/packages/encoding/src/bech32.ts +++ b/packages/encoding/src/bech32.ts @@ -16,6 +16,17 @@ export function fromBech32( }; } +/** + * Takes a bech32 address and returns a normalized (i.e. lower case) representation of it. + * + * The input is validated along the way, which makes this significantly safer than + * using `address.toLowerCase()`. + */ +export function normalizeBech32(address: string): string { + const { prefix, data } = fromBech32(address); + return toBech32(prefix, data); +} + /** * @deprecated This class is deprecated and will be removed soon. Please use fromBech32() and toBech32() instead. For more details please refer to https://github.com/cosmos/cosmjs/issues/1053. */