diff --git a/src/address.spec.ts b/src/address.spec.ts index f0a97792..1d138206 100644 --- a/src/address.spec.ts +++ b/src/address.spec.ts @@ -1,7 +1,7 @@ import { Address, Algorithm, PubkeyBytes } from "@iov/bcp"; import { Encoding } from "@iov/encoding"; -import { decodeCosmosAddress, isValidAddress, pubkeyToAddress } from "./address"; +import { decodeCosmosAddress, decodeCosmosPubkey, isValidAddress, pubkeyToAddress } from "./address"; const { fromBase64, fromHex } = Encoding; @@ -28,6 +28,17 @@ describe("address", () => { }); }); + describe("decodeCosmosPubkey", () => { + it("works", () => { + expect( + decodeCosmosPubkey("cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5"), + ).toEqual({ + prefix: "cosmospub", + data: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"), + }); + }); + }); + describe("isValidAddress", () => { it("accepts valid addresses", () => { expect(isValidAddress("cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6")).toEqual(true); diff --git a/src/address.ts b/src/address.ts index 853e24af..d36a50d4 100644 --- a/src/address.ts +++ b/src/address.ts @@ -1,15 +1,23 @@ import { Address, Algorithm, PubkeyBundle } from "@iov/bcp"; import { Ripemd160, Secp256k1, Sha256 } from "@iov/crypto"; -import { Bech32 } from "@iov/encoding"; +import { Bech32, Encoding } from "@iov/encoding"; +import equal from "fast-deep-equal"; export type CosmosAddressBech32Prefix = "cosmos" | "cosmosvalcons" | "cosmosvaloper"; export type CosmosPubkeyBech32Prefix = "cosmospub" | "cosmosvalconspub" | "cosmosvaloperpub"; export type CosmosBech32Prefix = CosmosAddressBech32Prefix | CosmosPubkeyBech32Prefix; +// As discussed in https://github.com/binance-chain/javascript-sdk/issues/163 +const pubkeyAminoPrefix = Encoding.fromHex("eb5ae98721"); + function isCosmosAddressBech32Prefix(prefix: string): prefix is CosmosAddressBech32Prefix { return ["cosmos", "cosmosvalcons", "cosmosvaloper"].includes(prefix); } +function isCosmosPubkeyBech32Prefix(prefix: string): prefix is CosmosPubkeyBech32Prefix { + return ["cosmospub", "cosmosvalconspub", "cosmosvaloperpub"].includes(prefix); +} + export function decodeCosmosAddress( address: Address, ): { readonly prefix: CosmosAddressBech32Prefix; readonly data: Uint8Array } { @@ -23,6 +31,26 @@ export function decodeCosmosAddress( return { prefix: prefix, data: data }; } +export function decodeCosmosPubkey( + encodedPubkey: string, +): { readonly prefix: CosmosPubkeyBech32Prefix; readonly data: Uint8Array } { + const { prefix, data } = Bech32.decode(encodedPubkey); + if (!isCosmosPubkeyBech32Prefix(prefix)) { + throw new Error(`Invalid bech32 prefix. Must be one of cosmos, cosmosvalcons, or cosmosvaloper.`); + } + + if (!equal(data.slice(0, pubkeyAminoPrefix.length), pubkeyAminoPrefix)) { + throw new Error("Pubkey does not have the expected amino prefix " + Encoding.toHex(pubkeyAminoPrefix)); + } + + const rest = data.slice(pubkeyAminoPrefix.length); + if (rest.length !== 33) { + throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey)."); + } + + return { prefix: prefix, data: rest }; +} + export function isValidAddress(address: string): boolean { try { decodeCosmosAddress(address as Address); diff --git a/src/cosmoscodec.ts b/src/cosmoscodec.ts index 5bce7c36..f4b3619d 100644 --- a/src/cosmoscodec.ts +++ b/src/cosmoscodec.ts @@ -22,7 +22,7 @@ import { CosmosBech32Prefix, isValidAddress, pubkeyToAddress } from "./address"; import { Caip5 } from "./caip5"; import { parseTx } from "./decode"; import { buildSignedTx, buildUnsignedTx } from "./encode"; -import { TokenInfos, nonceToAccountNumber, nonceToSequence } from "./types"; +import { nonceToAccountNumber, nonceToSequence, TokenInfos } from "./types"; const { toHex, toUtf8 } = Encoding; diff --git a/src/cosmosconnection.spec.ts b/src/cosmosconnection.spec.ts index d5a540eb..0a97cd34 100644 --- a/src/cosmosconnection.spec.ts +++ b/src/cosmosconnection.spec.ts @@ -17,7 +17,7 @@ import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol"; import { CosmosBech32Prefix } from "./address"; import { CosmosCodec, cosmosCodec } from "./cosmoscodec"; import { CosmosConnection } from "./cosmosconnection"; -import { TokenInfos, nonceToSequence } from "./types"; +import { nonceToSequence, TokenInfos } from "./types"; const { fromBase64, toHex } = Encoding; diff --git a/src/cosmosconnection.ts b/src/cosmosconnection.ts index 6646924e..fb5221fd 100644 --- a/src/cosmosconnection.ts +++ b/src/cosmosconnection.ts @@ -19,6 +19,7 @@ import { Nonce, PostableBytes, PostTxResponse, + PubkeyBundle, PubkeyBytes, PubkeyQuery, Token, @@ -27,19 +28,18 @@ import { TransactionQuery, TransactionState, UnsignedTransaction, - PubkeyBundle, } from "@iov/bcp"; -import { Encoding, Uint53, Bech32 } from "@iov/encoding"; +import { Bech32, Encoding, Uint53 } from "@iov/encoding"; import { DefaultValueProducer, ValueAndUpdates } from "@iov/stream"; import equal from "fast-deep-equal"; import { ReadonlyDate } from "readonly-date"; import { Stream } from "xstream"; -import { CosmosBech32Prefix, decodeCosmosAddress, pubkeyToAddress } from "./address"; +import { CosmosBech32Prefix, decodeCosmosPubkey, pubkeyToAddress } from "./address"; import { Caip5 } from "./caip5"; import { decodeAmount, parseTxsResponse } from "./decode"; import { RestClient, TxsResponse } from "./restclient"; -import { TokenInfos, accountToNonce } from "./types"; +import { accountToNonce, TokenInfos } from "./types"; const { fromBase64 } = Encoding; @@ -158,8 +158,7 @@ export class CosmosConnection implements BlockchainConnection { : { algo: Algorithm.Secp256k1, // amino-js has wrong (outdated) types - data: Bech32.decode(account.public_key as any).data as PubkeyBytes, - // data: decodeCosmosAddress(account.public_key).data as PubkeyBytes, + data: decodeCosmosPubkey(account.public_key as any).data as PubkeyBytes, }; return { address: address, diff --git a/types/address.d.ts b/types/address.d.ts index 776cb61d..252add6e 100644 --- a/types/address.d.ts +++ b/types/address.d.ts @@ -8,5 +8,11 @@ export declare function decodeCosmosAddress( readonly prefix: CosmosAddressBech32Prefix; readonly data: Uint8Array; }; +export declare function decodeCosmosPubkey( + encodedPubkey: string, +): { + readonly prefix: CosmosPubkeyBech32Prefix; + readonly data: Uint8Array; +}; export declare function isValidAddress(address: string): boolean; export declare function pubkeyToAddress(pubkey: PubkeyBundle, prefix: CosmosBech32Prefix): Address;