From cbf1a8f495804dfbee7350c3dbfa420d30ea8f84 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 16:16:43 +0100 Subject: [PATCH 01/12] launchpad: Remove empty pubkey files --- packages/launchpad/src/pubkey.spec.ts | 0 packages/launchpad/src/pubkey.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/launchpad/src/pubkey.spec.ts delete mode 100644 packages/launchpad/src/pubkey.ts diff --git a/packages/launchpad/src/pubkey.spec.ts b/packages/launchpad/src/pubkey.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/launchpad/src/pubkey.ts b/packages/launchpad/src/pubkey.ts deleted file mode 100644 index e69de29b..00000000 From 38a650f30764b6c015732cffc240dbca9c2a777f Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 16:35:12 +0100 Subject: [PATCH 02/12] amino: Transfer paths from launchpad --- packages/amino/src/paths.spec.ts | 26 ++++++++++++++++++++++++++ packages/amino/src/paths.ts | 15 +++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 packages/amino/src/paths.spec.ts create mode 100644 packages/amino/src/paths.ts diff --git a/packages/amino/src/paths.spec.ts b/packages/amino/src/paths.spec.ts new file mode 100644 index 00000000..17fd85b6 --- /dev/null +++ b/packages/amino/src/paths.spec.ts @@ -0,0 +1,26 @@ +import { Slip10RawIndex } from "@cosmjs/crypto"; + +import { makeCosmoshubPath } from "./paths"; + +describe("paths", () => { + describe("makeCosmoshubPath", () => { + it("works", () => { + // m/44'/118'/0'/0/0 + expect(makeCosmoshubPath(0)).toEqual([ + Slip10RawIndex.hardened(44), + Slip10RawIndex.hardened(118), + Slip10RawIndex.hardened(0), + Slip10RawIndex.normal(0), + Slip10RawIndex.normal(0), + ]); + // m/44'/118'/0'/0/123 + expect(makeCosmoshubPath(123)).toEqual([ + Slip10RawIndex.hardened(44), + Slip10RawIndex.hardened(118), + Slip10RawIndex.hardened(0), + Slip10RawIndex.normal(0), + Slip10RawIndex.normal(123), + ]); + }); + }); +}); diff --git a/packages/amino/src/paths.ts b/packages/amino/src/paths.ts new file mode 100644 index 00000000..bf4dd185 --- /dev/null +++ b/packages/amino/src/paths.ts @@ -0,0 +1,15 @@ +import { HdPath, Slip10RawIndex } from "@cosmjs/crypto"; + +/** + * The Cosmos Hub derivation path in the form `m/44'/118'/0'/0/a` + * with 0-based account index `a`. + */ +export function makeCosmoshubPath(a: number): HdPath { + return [ + Slip10RawIndex.hardened(44), + Slip10RawIndex.hardened(118), + Slip10RawIndex.hardened(0), + Slip10RawIndex.normal(0), + Slip10RawIndex.normal(a), + ]; +} From e4283c36de4070372e4e9d74d596bffa30da84af Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 16:35:34 +0100 Subject: [PATCH 03/12] amino: Transfer wallets from launchpad --- packages/amino/src/secp256k1hdwallet.spec.ts | 181 +++++++++++ packages/amino/src/secp256k1hdwallet.ts | 324 +++++++++++++++++++ packages/amino/src/secp256k1wallet.spec.ts | 54 ++++ packages/amino/src/secp256k1wallet.ts | 62 ++++ packages/amino/src/wallet.ts | 87 +++++ 5 files changed, 708 insertions(+) create mode 100644 packages/amino/src/secp256k1hdwallet.spec.ts create mode 100644 packages/amino/src/secp256k1hdwallet.ts create mode 100644 packages/amino/src/secp256k1wallet.spec.ts create mode 100644 packages/amino/src/secp256k1wallet.ts create mode 100644 packages/amino/src/wallet.ts diff --git a/packages/amino/src/secp256k1hdwallet.spec.ts b/packages/amino/src/secp256k1hdwallet.spec.ts new file mode 100644 index 00000000..d852df1f --- /dev/null +++ b/packages/amino/src/secp256k1hdwallet.spec.ts @@ -0,0 +1,181 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { Secp256k1, Secp256k1Signature, sha256 } from "@cosmjs/crypto"; +import { fromBase64, fromHex } from "@cosmjs/encoding"; + +import { extractKdfConfiguration, Secp256k1HdWallet } from "./secp256k1hdwallet"; +import { serializeSignDoc, StdSignDoc } from "./signdoc"; +import { executeKdf, KdfConfiguration } from "./wallet"; + +const base64Matcher = /^(?:[a-zA-Z0-9+/]{4})*(?:|(?:[a-zA-Z0-9+/]{3}=)|(?:[a-zA-Z0-9+/]{2}==)|(?:[a-zA-Z0-9+/]{1}===))$/; + +describe("Secp256k1HdWallet", () => { + // m/44'/118'/0'/0/0 + // pubkey: 02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6 + const defaultMnemonic = "special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling"; + const defaultPubkey = fromHex("02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6"); + const defaultAddress = "cosmos1jhg0e7s6gn44tfc5k37kr04sznyhedtc9rzys5"; + + describe("fromMnemonic", () => { + it("works", async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); + expect(wallet).toBeTruthy(); + expect(wallet.mnemonic).toEqual(defaultMnemonic); + }); + }); + + describe("generate", () => { + it("defaults to 12 words", async () => { + const wallet = await Secp256k1HdWallet.generate(); + expect(wallet.mnemonic.split(" ").length).toEqual(12); + }); + + it("can use different mnemonic lengths", async () => { + expect((await Secp256k1HdWallet.generate(12)).mnemonic.split(" ").length).toEqual(12); + expect((await Secp256k1HdWallet.generate(15)).mnemonic.split(" ").length).toEqual(15); + expect((await Secp256k1HdWallet.generate(18)).mnemonic.split(" ").length).toEqual(18); + expect((await Secp256k1HdWallet.generate(21)).mnemonic.split(" ").length).toEqual(21); + expect((await Secp256k1HdWallet.generate(24)).mnemonic.split(" ").length).toEqual(24); + }); + }); + + describe("deserialize", () => { + it("can restore", async () => { + const original = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); + const password = "123"; + const serialized = await original.serialize(password); + const deserialized = await Secp256k1HdWallet.deserialize(serialized, password); + expect(deserialized.mnemonic).toEqual(defaultMnemonic); + expect(await deserialized.getAccounts()).toEqual([ + { + algo: "secp256k1", + address: defaultAddress, + pubkey: defaultPubkey, + }, + ]); + }); + }); + + describe("deserializeWithEncryptionKey", () => { + it("can restore", async () => { + const password = "123"; + let serialized: string; + { + const original = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); + const anyKdfParams: KdfConfiguration = { + algorithm: "argon2id", + params: { + outputLength: 32, + opsLimit: 4, + memLimitKib: 3 * 1024, + }, + }; + const encryptionKey = await executeKdf(password, anyKdfParams); + serialized = await original.serializeWithEncryptionKey(encryptionKey, anyKdfParams); + } + + { + const kdfConfiguration = extractKdfConfiguration(serialized); + const encryptionKey = await executeKdf(password, kdfConfiguration); + const deserialized = await Secp256k1HdWallet.deserializeWithEncryptionKey(serialized, encryptionKey); + expect(deserialized.mnemonic).toEqual(defaultMnemonic); + expect(await deserialized.getAccounts()).toEqual([ + { + algo: "secp256k1", + address: defaultAddress, + pubkey: defaultPubkey, + }, + ]); + } + }); + }); + + describe("getAccounts", () => { + it("resolves to a list of accounts", async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); + const accounts = await wallet.getAccounts(); + expect(accounts.length).toEqual(1); + expect(accounts[0]).toEqual({ + address: defaultAddress, + algo: "secp256k1", + pubkey: defaultPubkey, + }); + }); + + it("creates the same address as Go implementation", async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic( + "oyster design unusual machine spread century engine gravity focus cave carry slot", + ); + const [{ address }] = await wallet.getAccounts(); + expect(address).toEqual("cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u"); + }); + }); + + describe("signAmino", () => { + it("resolves to valid signature", async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); + const signDoc: StdSignDoc = { + msgs: [], + fee: { amount: [], gas: "23" }, + chain_id: "foochain", + memo: "hello, world", + account_number: "7", + sequence: "54", + }; + const { signed, signature } = await wallet.signAmino(defaultAddress, signDoc); + expect(signed).toEqual(signDoc); + const valid = await Secp256k1.verifySignature( + Secp256k1Signature.fromFixedLength(fromBase64(signature.signature)), + sha256(serializeSignDoc(signed)), + defaultPubkey, + ); + expect(valid).toEqual(true); + }); + }); + + describe("serialize", () => { + it("can save with password", async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); + const serialized = await wallet.serialize("123"); + expect(JSON.parse(serialized)).toEqual({ + type: "secp256k1wallet-v1", + kdf: { + algorithm: "argon2id", + params: { + outputLength: 32, + opsLimit: 20, + memLimitKib: 12 * 1024, + }, + }, + encryption: { + algorithm: "xchacha20poly1305-ietf", + }, + data: jasmine.stringMatching(base64Matcher), + }); + }); + }); + + describe("serializeWithEncryptionKey", () => { + it("can save with password", async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); + + const key = fromHex("aabb221100aabb332211aabb33221100aabb221100aabb332211aabb33221100"); + const customKdfConfiguration: KdfConfiguration = { + algorithm: "argon2id", + params: { + outputLength: 32, + opsLimit: 321, + memLimitKib: 11 * 1024, + }, + }; + const serialized = await wallet.serializeWithEncryptionKey(key, customKdfConfiguration); + expect(JSON.parse(serialized)).toEqual({ + type: "secp256k1wallet-v1", + kdf: customKdfConfiguration, + encryption: { + algorithm: "xchacha20poly1305-ietf", + }, + data: jasmine.stringMatching(base64Matcher), + }); + }); + }); +}); diff --git a/packages/amino/src/secp256k1hdwallet.ts b/packages/amino/src/secp256k1hdwallet.ts new file mode 100644 index 00000000..1a2b6116 --- /dev/null +++ b/packages/amino/src/secp256k1hdwallet.ts @@ -0,0 +1,324 @@ +import { + Bip39, + EnglishMnemonic, + HdPath, + pathToString, + Random, + Secp256k1, + sha256, + Slip10, + Slip10Curve, + stringToPath, +} from "@cosmjs/crypto"; +import { Bech32, fromBase64, fromUtf8, toBase64, toUtf8 } from "@cosmjs/encoding"; +import { assert, isNonNullObject } from "@cosmjs/utils"; + +import { rawSecp256k1PubkeyToRawAddress } from "./addresses"; +import { makeCosmoshubPath } from "./paths"; +import { encodeSecp256k1Signature } from "./signature"; +import { serializeSignDoc, StdSignDoc } from "./signdoc"; +import { AccountData, AminoSignResponse, OfflineAminoSigner } from "./signer"; +import { + decrypt, + encrypt, + EncryptionConfiguration, + executeKdf, + KdfConfiguration, + supportedAlgorithms, +} from "./wallet"; + +const serializationTypeV1 = "secp256k1wallet-v1"; + +/** + * A KDF configuration that is not very strong but can be used on the main thread. + * It takes about 1 second in Node.js 12.15 and should have similar runtimes in other modern Wasm hosts. + */ +const basicPasswordHashingOptions: KdfConfiguration = { + algorithm: "argon2id", + params: { + outputLength: 32, + opsLimit: 20, + memLimitKib: 12 * 1024, + }, +}; + +/** + * This interface describes a JSON object holding the encrypted wallet and the meta data. + * All fields in here must be JSON types. + */ +export interface Secp256k1HdWalletSerialization { + /** A format+version identifier for this serialization format */ + readonly type: string; + /** Information about the key derivation function (i.e. password to encryption key) */ + readonly kdf: KdfConfiguration; + /** Information about the symmetric encryption */ + readonly encryption: EncryptionConfiguration; + /** An instance of Secp256k1HdWalletData, which is stringified, encrypted and base64 encoded. */ + readonly data: string; +} + +/** + * Derivation information required to derive a keypair and an address from a mnemonic. + * All fields in here must be JSON types. + */ +interface DerivationInfoJson { + readonly hdPath: string; + readonly prefix: string; +} + +function isDerivationJson(thing: unknown): thing is DerivationInfoJson { + if (!isNonNullObject(thing)) return false; + if (typeof (thing as DerivationInfoJson).hdPath !== "string") return false; + if (typeof (thing as DerivationInfoJson).prefix !== "string") return false; + return true; +} + +/** + * The data of a wallet serialization that is encrypted. + * All fields in here must be JSON types. + */ +interface Secp256k1HdWalletData { + readonly mnemonic: string; + readonly accounts: readonly DerivationInfoJson[]; +} + +function extractKdfConfigurationV1(doc: any): KdfConfiguration { + return doc.kdf; +} + +export function extractKdfConfiguration(serialization: string): KdfConfiguration { + const root = JSON.parse(serialization); + if (!isNonNullObject(root)) throw new Error("Root document is not an object."); + + switch ((root as any).type) { + case serializationTypeV1: + return extractKdfConfigurationV1(root); + default: + throw new Error("Unsupported serialization type"); + } +} + +/** + * Derivation information required to derive a keypair and an address from a mnemonic. + */ +interface DerivationInfo { + readonly hdPath: HdPath; + /** The bech32 address prefix (human readable part). */ + readonly prefix: string; +} + +export class Secp256k1HdWallet implements OfflineAminoSigner { + /** + * Restores a wallet from the given BIP39 mnemonic. + * + * @param mnemonic Any valid English mnemonic. + * @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. + * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". + */ + public static async fromMnemonic( + mnemonic: string, + hdPath: HdPath = makeCosmoshubPath(0), + prefix = "cosmos", + ): Promise { + const mnemonicChecked = new EnglishMnemonic(mnemonic); + const seed = await Bip39.mnemonicToSeed(mnemonicChecked); + const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath); + const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; + return new Secp256k1HdWallet( + mnemonicChecked, + hdPath, + privkey, + Secp256k1.compressPubkey(uncompressed), + prefix, + ); + } + + /** + * Generates a new wallet with a BIP39 mnemonic of the given length. + * + * @param length The number of words in the mnemonic (12, 15, 18, 21 or 24). + * @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. + * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". + */ + public static async generate( + length: 12 | 15 | 18 | 21 | 24 = 12, + hdPath: HdPath = makeCosmoshubPath(0), + prefix = "cosmos", + ): Promise { + const entropyLength = 4 * Math.floor((11 * length) / 33); + const entropy = Random.getBytes(entropyLength); + const mnemonic = Bip39.encode(entropy); + return Secp256k1HdWallet.fromMnemonic(mnemonic.toString(), hdPath, prefix); + } + + /** + * Restores a wallet from an encrypted serialization. + * + * @param password The user provided password used to generate an encryption key via a KDF. + * This is not normalized internally (see "Unicode normalization" to learn more). + */ + public static async deserialize(serialization: string, password: string): Promise { + const root = JSON.parse(serialization); + if (!isNonNullObject(root)) throw new Error("Root document is not an object."); + switch ((root as any).type) { + case serializationTypeV1: + return Secp256k1HdWallet.deserializeTypeV1(serialization, password); + default: + throw new Error("Unsupported serialization type"); + } + } + + /** + * Restores a wallet from an encrypted serialization. + * + * This is an advanced alternative to calling `deserialize(serialization, password)` directly, which allows + * you to offload the KDF execution to a non-UI thread (e.g. in a WebWorker). + * + * The caller is responsible for ensuring the key was derived with the given KDF configuration. This can be + * done using `extractKdfConfiguration(serialization)` and `executeKdf(password, kdfConfiguration)` from this package. + */ + public static async deserializeWithEncryptionKey( + serialization: string, + encryptionKey: Uint8Array, + ): Promise { + const root = JSON.parse(serialization); + if (!isNonNullObject(root)) throw new Error("Root document is not an object."); + const untypedRoot: any = root; + switch (untypedRoot.type) { + case serializationTypeV1: { + const decryptedBytes = await decrypt( + fromBase64(untypedRoot.data), + encryptionKey, + untypedRoot.encryption, + ); + const decryptedDocument = JSON.parse(fromUtf8(decryptedBytes)); + const { mnemonic, accounts } = decryptedDocument; + assert(typeof mnemonic === "string"); + if (!Array.isArray(accounts)) throw new Error("Property 'accounts' is not an array"); + if (accounts.length !== 1) throw new Error("Property 'accounts' only supports one entry"); + const account = accounts[0]; + if (!isDerivationJson(account)) throw new Error("Account is not in the correct format."); + return Secp256k1HdWallet.fromMnemonic(mnemonic, stringToPath(account.hdPath), account.prefix); + } + default: + throw new Error("Unsupported serialization type"); + } + } + + private static async deserializeTypeV1( + serialization: string, + password: string, + ): Promise { + const root = JSON.parse(serialization); + if (!isNonNullObject(root)) throw new Error("Root document is not an object."); + const encryptionKey = await executeKdf(password, (root as any).kdf); + return Secp256k1HdWallet.deserializeWithEncryptionKey(serialization, encryptionKey); + } + + /** Base secret */ + private readonly secret: EnglishMnemonic; + /** Derivation instruction */ + private readonly accounts: readonly DerivationInfo[]; + /** Derived data */ + private readonly pubkey: Uint8Array; + private readonly privkey: Uint8Array; + + protected constructor( + mnemonic: EnglishMnemonic, + hdPath: HdPath, + privkey: Uint8Array, + pubkey: Uint8Array, + prefix: string, + ) { + this.secret = mnemonic; + this.accounts = [ + { + hdPath: hdPath, + prefix: prefix, + }, + ]; + this.privkey = privkey; + this.pubkey = pubkey; + } + + public get mnemonic(): string { + return this.secret.toString(); + } + + private get address(): string { + return Bech32.encode(this.accounts[0].prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey)); + } + + public async getAccounts(): Promise { + return [ + { + algo: "secp256k1", + address: this.address, + pubkey: this.pubkey, + }, + ]; + } + + public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise { + if (signerAddress !== this.address) { + throw new Error(`Address ${signerAddress} not found in wallet`); + } + const message = sha256(serializeSignDoc(signDoc)); + const signature = await Secp256k1.createSignature(message, this.privkey); + const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]); + return { + signed: signDoc, + signature: encodeSecp256k1Signature(this.pubkey, signatureBytes), + }; + } + + /** + * Generates an encrypted serialization of this wallet. + * + * @param password The user provided password used to generate an encryption key via a KDF. + * This is not normalized internally (see "Unicode normalization" to learn more). + */ + public async serialize(password: string): Promise { + const kdfConfiguration = basicPasswordHashingOptions; + const encryptionKey = await executeKdf(password, kdfConfiguration); + return this.serializeWithEncryptionKey(encryptionKey, kdfConfiguration); + } + + /** + * Generates an encrypted serialization of this wallet. + * + * This is an advanced alternative to calling `serialize(password)` directly, which allows you to + * offload the KDF execution to a non-UI thread (e.g. in a WebWorker). + * + * The caller is responsible for ensuring the key was derived with the given KDF options. If this + * is not the case, the wallet cannot be restored with the original password. + */ + public async serializeWithEncryptionKey( + encryptionKey: Uint8Array, + kdfConfiguration: KdfConfiguration, + ): Promise { + const dataToEncrypt: Secp256k1HdWalletData = { + mnemonic: this.mnemonic, + accounts: this.accounts.map( + (account): DerivationInfoJson => ({ + hdPath: pathToString(account.hdPath), + prefix: account.prefix, + }), + ), + }; + const dataToEncryptRaw = toUtf8(JSON.stringify(dataToEncrypt)); + + const encryptionConfiguration: EncryptionConfiguration = { + algorithm: supportedAlgorithms.xchacha20poly1305Ietf, + }; + const encryptedData = await encrypt(dataToEncryptRaw, encryptionKey, encryptionConfiguration); + + const out: Secp256k1HdWalletSerialization = { + type: serializationTypeV1, + kdf: kdfConfiguration, + encryption: encryptionConfiguration, + data: toBase64(encryptedData), + }; + return JSON.stringify(out); + } +} diff --git a/packages/amino/src/secp256k1wallet.spec.ts b/packages/amino/src/secp256k1wallet.spec.ts new file mode 100644 index 00000000..f2643275 --- /dev/null +++ b/packages/amino/src/secp256k1wallet.spec.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto"; +import { fromBase64, fromHex } from "@cosmjs/encoding"; + +import { Secp256k1Wallet } from "./secp256k1wallet"; +import { serializeSignDoc, StdSignDoc } from "./signdoc"; + +describe("Secp256k1Wallet", () => { + const defaultPrivkey = fromHex("b8c462d2bb0c1a92edf44f735021f16c270f28ee2c3d1cb49943a5e70a3c763e"); + const defaultAddress = "cosmos1kxt5x5q2l57ma2d434pqpafxdm0mgeg9c8cvtx"; + const defaultPubkey = fromHex("03f146c27639179e5b67b8646108f48e1a78b146c74939e34afaa5414ad5c93f8a"); + + describe("fromKey", () => { + it("works", async () => { + const signer = await Secp256k1Wallet.fromKey(defaultPrivkey); + expect(signer).toBeTruthy(); + }); + }); + + describe("getAccounts", () => { + it("resolves to a list of accounts", async () => { + const signer = await Secp256k1Wallet.fromKey(defaultPrivkey); + const accounts = await signer.getAccounts(); + expect(accounts.length).toEqual(1); + expect(accounts[0]).toEqual({ + address: defaultAddress, + algo: "secp256k1", + pubkey: defaultPubkey, + }); + }); + }); + + describe("signAmino", () => { + it("resolves to valid signature", async () => { + const signer = await Secp256k1Wallet.fromKey(defaultPrivkey); + const signDoc: StdSignDoc = { + msgs: [], + fee: { amount: [], gas: "23" }, + chain_id: "foochain", + memo: "hello, world", + account_number: "7", + sequence: "54", + }; + const { signed, signature } = await signer.signAmino(defaultAddress, signDoc); + expect(signed).toEqual(signDoc); + const valid = await Secp256k1.verifySignature( + Secp256k1Signature.fromFixedLength(fromBase64(signature.signature)), + new Sha256(serializeSignDoc(signed)).digest(), + defaultPubkey, + ); + expect(valid).toEqual(true); + }); + }); +}); diff --git a/packages/amino/src/secp256k1wallet.ts b/packages/amino/src/secp256k1wallet.ts new file mode 100644 index 00000000..d4b7bb55 --- /dev/null +++ b/packages/amino/src/secp256k1wallet.ts @@ -0,0 +1,62 @@ +import { Secp256k1, Sha256 } from "@cosmjs/crypto"; +import { Bech32 } from "@cosmjs/encoding"; + +import { rawSecp256k1PubkeyToRawAddress } from "./addresses"; +import { encodeSecp256k1Signature } from "./signature"; +import { serializeSignDoc, StdSignDoc } from "./signdoc"; +import { AccountData, AminoSignResponse, OfflineAminoSigner } from "./signer"; + +/** + * A wallet that holds a single secp256k1 keypair. + * + * If you want to work with BIP39 mnemonics and multiple accounts, use Secp256k1HdWallet. + */ +export class Secp256k1Wallet implements OfflineAminoSigner { + /** + * Creates a Secp256k1Wallet from the given private key + * + * @param privkey The private key. + * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". + */ + public static async fromKey(privkey: Uint8Array, prefix = "cosmos"): Promise { + const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; + return new Secp256k1Wallet(privkey, Secp256k1.compressPubkey(uncompressed), prefix); + } + + private readonly pubkey: Uint8Array; + private readonly privkey: Uint8Array; + private readonly prefix: string; + + private constructor(privkey: Uint8Array, pubkey: Uint8Array, prefix: string) { + this.privkey = privkey; + this.pubkey = pubkey; + this.prefix = prefix; + } + + private get address(): string { + return Bech32.encode(this.prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey)); + } + + public async getAccounts(): Promise { + return [ + { + algo: "secp256k1", + address: this.address, + pubkey: this.pubkey, + }, + ]; + } + + public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise { + if (signerAddress !== this.address) { + throw new Error(`Address ${signerAddress} not found in wallet`); + } + const message = new Sha256(serializeSignDoc(signDoc)).digest(); + const signature = await Secp256k1.createSignature(message, this.privkey); + const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]); + return { + signed: signDoc, + signature: encodeSecp256k1Signature(this.pubkey, signatureBytes), + }; + } +} diff --git a/packages/amino/src/wallet.ts b/packages/amino/src/wallet.ts new file mode 100644 index 00000000..dc85068a --- /dev/null +++ b/packages/amino/src/wallet.ts @@ -0,0 +1,87 @@ +import { + Argon2id, + isArgon2idOptions, + Random, + xchacha20NonceLength, + Xchacha20poly1305Ietf, +} from "@cosmjs/crypto"; +import { toAscii } from "@cosmjs/encoding"; + +/** + * A fixed salt is chosen to archive a deterministic password to key derivation. + * This reduces the scope of a potential rainbow attack to all CosmJS users. + * Must be 16 bytes due to implementation limitations. + */ +export const cosmjsSalt = toAscii("The CosmJS salt."); + +export interface KdfConfiguration { + /** + * An algorithm identifier, such as "argon2id" or "scrypt". + */ + readonly algorithm: string; + /** A map of algorithm-specific parameters */ + readonly params: Record; +} + +export async function executeKdf(password: string, configuration: KdfConfiguration): Promise { + switch (configuration.algorithm) { + case "argon2id": { + const options = configuration.params; + if (!isArgon2idOptions(options)) throw new Error("Invalid format of argon2id params"); + return Argon2id.execute(password, cosmjsSalt, options); + } + default: + throw new Error("Unsupported KDF algorithm"); + } +} + +/** + * Configuration how to encrypt data or how data was encrypted. + * This is stored as part of the wallet serialization and must only contain JSON types. + */ +export interface EncryptionConfiguration { + /** + * An algorithm identifier, such as "xchacha20poly1305-ietf". + */ + readonly algorithm: string; + /** A map of algorithm-specific parameters */ + readonly params?: Record; +} + +export const supportedAlgorithms = { + xchacha20poly1305Ietf: "xchacha20poly1305-ietf", +}; + +export async function encrypt( + plaintext: Uint8Array, + encryptionKey: Uint8Array, + config: EncryptionConfiguration, +): Promise { + switch (config.algorithm) { + case supportedAlgorithms.xchacha20poly1305Ietf: { + const nonce = Random.getBytes(xchacha20NonceLength); + // Prepend fixed-length nonce to ciphertext as suggested in the example from https://github.com/jedisct1/libsodium.js#api + return new Uint8Array([ + ...nonce, + ...(await Xchacha20poly1305Ietf.encrypt(plaintext, encryptionKey, nonce)), + ]); + } + default: + throw new Error(`Unsupported encryption algorithm: '${config.algorithm}'`); + } +} + +export async function decrypt( + ciphertext: Uint8Array, + encryptionKey: Uint8Array, + config: EncryptionConfiguration, +): Promise { + switch (config.algorithm) { + case supportedAlgorithms.xchacha20poly1305Ietf: { + const nonce = ciphertext.slice(0, xchacha20NonceLength); + return Xchacha20poly1305Ietf.decrypt(ciphertext.slice(xchacha20NonceLength), encryptionKey, nonce); + } + default: + throw new Error(`Unsupported encryption algorithm: '${config.algorithm}'`); + } +} From dde8334f21a5dd7200520f2ad75d11e3718eba75 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 16:37:10 +0100 Subject: [PATCH 04/12] amino: Add exports for paths/wallets --- packages/amino/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index c45fd5bb..f6f8fc85 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -19,6 +19,10 @@ export { pubkeyType, } from "./pubkeys"; export { createMultisigThresholdPubkey } from "./multisig"; +export { makeCosmoshubPath } from "./paths"; +export { extractKdfConfiguration, Secp256k1HdWallet } from "./secp256k1hdwallet"; +export { Secp256k1Wallet } from "./secp256k1wallet"; export { decodeSignature, encodeSecp256k1Signature, StdSignature } from "./signature"; export { AminoMsg, Coin, makeSignDoc, serializeSignDoc, StdFee, StdSignDoc } from "./signdoc"; export { AccountData, Algo, AminoSignResponse, OfflineAminoSigner } from "./signer"; +export { executeKdf, KdfConfiguration } from "./wallet"; From edfcc704eb641ca6442b0ea7b77f4855b4331c5b Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 16:45:20 +0100 Subject: [PATCH 05/12] launchpad: Use paths/wallets from amino --- .../src/cosmosclient.searchtx.spec.ts | 3 +- packages/launchpad/src/cosmosclient.spec.ts | 3 +- packages/launchpad/src/index.ts | 10 +- .../launchpad/src/lcdapi/distribution.spec.ts | 3 +- packages/launchpad/src/lcdapi/gov.spec.ts | 3 +- .../launchpad/src/lcdapi/lcdclient.spec.ts | 4 +- packages/launchpad/src/lcdapi/staking.spec.ts | 3 +- packages/launchpad/src/paths.spec.ts | 26 -- packages/launchpad/src/paths.ts | 15 - .../launchpad/src/secp256k1hdwallet.spec.ts | 180 ---------- packages/launchpad/src/secp256k1hdwallet.ts | 329 ------------------ .../launchpad/src/secp256k1wallet.spec.ts | 54 --- packages/launchpad/src/secp256k1wallet.ts | 66 ---- .../launchpad/src/signingcosmosclient.spec.ts | 4 +- packages/launchpad/src/wallet.ts | 87 ----- 15 files changed, 13 insertions(+), 777 deletions(-) delete mode 100644 packages/launchpad/src/paths.spec.ts delete mode 100644 packages/launchpad/src/paths.ts delete mode 100644 packages/launchpad/src/secp256k1hdwallet.spec.ts delete mode 100644 packages/launchpad/src/secp256k1hdwallet.ts delete mode 100644 packages/launchpad/src/secp256k1wallet.spec.ts delete mode 100644 packages/launchpad/src/secp256k1wallet.ts delete mode 100644 packages/launchpad/src/wallet.ts diff --git a/packages/launchpad/src/cosmosclient.searchtx.spec.ts b/packages/launchpad/src/cosmosclient.searchtx.spec.ts index dc643b12..5fa17338 100644 --- a/packages/launchpad/src/cosmosclient.searchtx.spec.ts +++ b/packages/launchpad/src/cosmosclient.searchtx.spec.ts @@ -1,12 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { makeSignDoc } from "@cosmjs/amino"; +import { makeSignDoc, Secp256k1HdWallet } from "@cosmjs/amino"; import { assert, sleep } from "@cosmjs/utils"; import { coins } from "./coins"; import { CosmosClient, isBroadcastTxFailure } from "./cosmosclient"; import { LcdClient } from "./lcdapi"; import { isMsgSend, MsgSend } from "./msgs"; -import { Secp256k1HdWallet } from "./secp256k1hdwallet"; import { SigningCosmosClient } from "./signingcosmosclient"; import { faucet, diff --git a/packages/launchpad/src/cosmosclient.spec.ts b/packages/launchpad/src/cosmosclient.spec.ts index db00aa23..84a4e110 100644 --- a/packages/launchpad/src/cosmosclient.spec.ts +++ b/packages/launchpad/src/cosmosclient.spec.ts @@ -1,12 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { makeSignDoc, StdFee } from "@cosmjs/amino"; +import { makeSignDoc, Secp256k1HdWallet, StdFee } from "@cosmjs/amino"; import { assert, sleep } from "@cosmjs/utils"; import { ReadonlyDate } from "readonly-date"; import { assertIsBroadcastTxSuccess, CosmosClient, PrivateCosmosClient } from "./cosmosclient"; import { findAttribute } from "./logs"; import { MsgSend } from "./msgs"; -import { Secp256k1HdWallet } from "./secp256k1hdwallet"; import cosmoshub from "./testdata/cosmoshub.json"; import { faucet, diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index 7d6219fb..6231d334 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -5,7 +5,10 @@ export { AminoMsg as Msg, AminoSignResponse, Coin, + KdfConfiguration, OfflineAminoSigner as OfflineSigner, + Secp256k1HdWallet, + Secp256k1Wallet, StdFee, StdSignDoc, StdSignature, @@ -16,6 +19,9 @@ export { encodeBech32Pubkey, encodeSecp256k1Pubkey, encodeSecp256k1Signature, + extractKdfConfiguration, + executeKdf, + makeCosmoshubPath, makeSignDoc, pubkeyToAddress, pubkeyType, @@ -147,10 +153,6 @@ export { MsgWithdrawDelegatorReward, MsgWithdrawValidatorCommission, } from "./msgs"; -export { makeCosmoshubPath } from "./paths"; export { findSequenceForSignedTx } from "./sequence"; export { CosmosFeeTable, SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, isWrappedStdTx, makeStdTx, CosmosSdkTx, StdTx, WrappedStdTx, WrappedTx } from "./tx"; -export { executeKdf, KdfConfiguration } from "./wallet"; -export { extractKdfConfiguration, Secp256k1HdWallet } from "./secp256k1hdwallet"; -export { Secp256k1Wallet } from "./secp256k1wallet"; diff --git a/packages/launchpad/src/lcdapi/distribution.spec.ts b/packages/launchpad/src/lcdapi/distribution.spec.ts index 667219ad..c28b34f1 100644 --- a/packages/launchpad/src/lcdapi/distribution.spec.ts +++ b/packages/launchpad/src/lcdapi/distribution.spec.ts @@ -1,12 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { makeSignDoc } from "@cosmjs/amino"; +import { makeSignDoc, Secp256k1HdWallet } from "@cosmjs/amino"; import { Bech32 } from "@cosmjs/encoding"; import { sleep } from "@cosmjs/utils"; import { coin, coins } from "../coins"; import { assertIsBroadcastTxSuccess } from "../cosmosclient"; import { MsgDelegate } from "../msgs"; -import { Secp256k1HdWallet } from "../secp256k1hdwallet"; import { SigningCosmosClient } from "../signingcosmosclient"; import { bigDecimalMatcher, diff --git a/packages/launchpad/src/lcdapi/gov.spec.ts b/packages/launchpad/src/lcdapi/gov.spec.ts index f907c888..94fe81d3 100644 --- a/packages/launchpad/src/lcdapi/gov.spec.ts +++ b/packages/launchpad/src/lcdapi/gov.spec.ts @@ -1,10 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { makeSignDoc } from "@cosmjs/amino"; +import { makeSignDoc, Secp256k1HdWallet } from "@cosmjs/amino"; import { sleep } from "@cosmjs/utils"; import { coins } from "../coins"; import { assertIsBroadcastTxSuccess } from "../cosmosclient"; -import { Secp256k1HdWallet } from "../secp256k1hdwallet"; import { SigningCosmosClient } from "../signingcosmosclient"; import { dateTimeStampMatcher, diff --git a/packages/launchpad/src/lcdapi/lcdclient.spec.ts b/packages/launchpad/src/lcdapi/lcdclient.spec.ts index d10cc24d..4a8e0ee4 100644 --- a/packages/launchpad/src/lcdapi/lcdclient.spec.ts +++ b/packages/launchpad/src/lcdapi/lcdclient.spec.ts @@ -1,12 +1,10 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { Coin, makeSignDoc, StdFee } from "@cosmjs/amino"; +import { Coin, makeCosmoshubPath, makeSignDoc, Secp256k1HdWallet, StdFee } from "@cosmjs/amino"; import { assert, sleep } from "@cosmjs/utils"; import { isBroadcastTxFailure } from "../cosmosclient"; import { parseLogs } from "../logs"; import { MsgSend } from "../msgs"; -import { makeCosmoshubPath } from "../paths"; -import { Secp256k1HdWallet } from "../secp256k1hdwallet"; import { SigningCosmosClient } from "../signingcosmosclient"; import cosmoshub from "../testdata/cosmoshub.json"; import { diff --git a/packages/launchpad/src/lcdapi/staking.spec.ts b/packages/launchpad/src/lcdapi/staking.spec.ts index 6ec3ab1f..0dcf12c1 100644 --- a/packages/launchpad/src/lcdapi/staking.spec.ts +++ b/packages/launchpad/src/lcdapi/staking.spec.ts @@ -1,11 +1,10 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { makeSignDoc } from "@cosmjs/amino"; +import { makeSignDoc, Secp256k1HdWallet } from "@cosmjs/amino"; import { assert, sleep } from "@cosmjs/utils"; import { coin, coins } from "../coins"; import { assertIsBroadcastTxSuccess } from "../cosmosclient"; import { MsgDelegate, MsgUndelegate } from "../msgs"; -import { Secp256k1HdWallet } from "../secp256k1hdwallet"; import { SigningCosmosClient } from "../signingcosmosclient"; import { bigDecimalMatcher, diff --git a/packages/launchpad/src/paths.spec.ts b/packages/launchpad/src/paths.spec.ts deleted file mode 100644 index 17fd85b6..00000000 --- a/packages/launchpad/src/paths.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Slip10RawIndex } from "@cosmjs/crypto"; - -import { makeCosmoshubPath } from "./paths"; - -describe("paths", () => { - describe("makeCosmoshubPath", () => { - it("works", () => { - // m/44'/118'/0'/0/0 - expect(makeCosmoshubPath(0)).toEqual([ - Slip10RawIndex.hardened(44), - Slip10RawIndex.hardened(118), - Slip10RawIndex.hardened(0), - Slip10RawIndex.normal(0), - Slip10RawIndex.normal(0), - ]); - // m/44'/118'/0'/0/123 - expect(makeCosmoshubPath(123)).toEqual([ - Slip10RawIndex.hardened(44), - Slip10RawIndex.hardened(118), - Slip10RawIndex.hardened(0), - Slip10RawIndex.normal(0), - Slip10RawIndex.normal(123), - ]); - }); - }); -}); diff --git a/packages/launchpad/src/paths.ts b/packages/launchpad/src/paths.ts deleted file mode 100644 index bf4dd185..00000000 --- a/packages/launchpad/src/paths.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { HdPath, Slip10RawIndex } from "@cosmjs/crypto"; - -/** - * The Cosmos Hub derivation path in the form `m/44'/118'/0'/0/a` - * with 0-based account index `a`. - */ -export function makeCosmoshubPath(a: number): HdPath { - return [ - Slip10RawIndex.hardened(44), - Slip10RawIndex.hardened(118), - Slip10RawIndex.hardened(0), - Slip10RawIndex.normal(0), - Slip10RawIndex.normal(a), - ]; -} diff --git a/packages/launchpad/src/secp256k1hdwallet.spec.ts b/packages/launchpad/src/secp256k1hdwallet.spec.ts deleted file mode 100644 index e64e5aaf..00000000 --- a/packages/launchpad/src/secp256k1hdwallet.spec.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { serializeSignDoc, StdSignDoc } from "@cosmjs/amino"; -import { Secp256k1, Secp256k1Signature, sha256 } from "@cosmjs/crypto"; -import { fromBase64, fromHex } from "@cosmjs/encoding"; - -import { extractKdfConfiguration, Secp256k1HdWallet } from "./secp256k1hdwallet"; -import { base64Matcher } from "./testutils.spec"; -import { executeKdf, KdfConfiguration } from "./wallet"; - -describe("Secp256k1HdWallet", () => { - // m/44'/118'/0'/0/0 - // pubkey: 02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6 - const defaultMnemonic = "special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling"; - const defaultPubkey = fromHex("02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6"); - const defaultAddress = "cosmos1jhg0e7s6gn44tfc5k37kr04sznyhedtc9rzys5"; - - describe("fromMnemonic", () => { - it("works", async () => { - const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); - expect(wallet).toBeTruthy(); - expect(wallet.mnemonic).toEqual(defaultMnemonic); - }); - }); - - describe("generate", () => { - it("defaults to 12 words", async () => { - const wallet = await Secp256k1HdWallet.generate(); - expect(wallet.mnemonic.split(" ").length).toEqual(12); - }); - - it("can use different mnemonic lengths", async () => { - expect((await Secp256k1HdWallet.generate(12)).mnemonic.split(" ").length).toEqual(12); - expect((await Secp256k1HdWallet.generate(15)).mnemonic.split(" ").length).toEqual(15); - expect((await Secp256k1HdWallet.generate(18)).mnemonic.split(" ").length).toEqual(18); - expect((await Secp256k1HdWallet.generate(21)).mnemonic.split(" ").length).toEqual(21); - expect((await Secp256k1HdWallet.generate(24)).mnemonic.split(" ").length).toEqual(24); - }); - }); - - describe("deserialize", () => { - it("can restore", async () => { - const original = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); - const password = "123"; - const serialized = await original.serialize(password); - const deserialized = await Secp256k1HdWallet.deserialize(serialized, password); - expect(deserialized.mnemonic).toEqual(defaultMnemonic); - expect(await deserialized.getAccounts()).toEqual([ - { - algo: "secp256k1", - address: defaultAddress, - pubkey: defaultPubkey, - }, - ]); - }); - }); - - describe("deserializeWithEncryptionKey", () => { - it("can restore", async () => { - const password = "123"; - let serialized: string; - { - const original = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); - const anyKdfParams: KdfConfiguration = { - algorithm: "argon2id", - params: { - outputLength: 32, - opsLimit: 4, - memLimitKib: 3 * 1024, - }, - }; - const encryptionKey = await executeKdf(password, anyKdfParams); - serialized = await original.serializeWithEncryptionKey(encryptionKey, anyKdfParams); - } - - { - const kdfConfiguration = extractKdfConfiguration(serialized); - const encryptionKey = await executeKdf(password, kdfConfiguration); - const deserialized = await Secp256k1HdWallet.deserializeWithEncryptionKey(serialized, encryptionKey); - expect(deserialized.mnemonic).toEqual(defaultMnemonic); - expect(await deserialized.getAccounts()).toEqual([ - { - algo: "secp256k1", - address: defaultAddress, - pubkey: defaultPubkey, - }, - ]); - } - }); - }); - - describe("getAccounts", () => { - it("resolves to a list of accounts", async () => { - const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); - const accounts = await wallet.getAccounts(); - expect(accounts.length).toEqual(1); - expect(accounts[0]).toEqual({ - address: defaultAddress, - algo: "secp256k1", - pubkey: defaultPubkey, - }); - }); - - it("creates the same address as Go implementation", async () => { - const wallet = await Secp256k1HdWallet.fromMnemonic( - "oyster design unusual machine spread century engine gravity focus cave carry slot", - ); - const [{ address }] = await wallet.getAccounts(); - expect(address).toEqual("cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u"); - }); - }); - - describe("signAmino", () => { - it("resolves to valid signature", async () => { - const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); - const signDoc: StdSignDoc = { - msgs: [], - fee: { amount: [], gas: "23" }, - chain_id: "foochain", - memo: "hello, world", - account_number: "7", - sequence: "54", - }; - const { signed, signature } = await wallet.signAmino(defaultAddress, signDoc); - expect(signed).toEqual(signDoc); - const valid = await Secp256k1.verifySignature( - Secp256k1Signature.fromFixedLength(fromBase64(signature.signature)), - sha256(serializeSignDoc(signed)), - defaultPubkey, - ); - expect(valid).toEqual(true); - }); - }); - - describe("serialize", () => { - it("can save with password", async () => { - const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); - const serialized = await wallet.serialize("123"); - expect(JSON.parse(serialized)).toEqual({ - type: "secp256k1wallet-v1", - kdf: { - algorithm: "argon2id", - params: { - outputLength: 32, - opsLimit: 20, - memLimitKib: 12 * 1024, - }, - }, - encryption: { - algorithm: "xchacha20poly1305-ietf", - }, - data: jasmine.stringMatching(base64Matcher), - }); - }); - }); - - describe("serializeWithEncryptionKey", () => { - it("can save with password", async () => { - const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); - - const key = fromHex("aabb221100aabb332211aabb33221100aabb221100aabb332211aabb33221100"); - const customKdfConfiguration: KdfConfiguration = { - algorithm: "argon2id", - params: { - outputLength: 32, - opsLimit: 321, - memLimitKib: 11 * 1024, - }, - }; - const serialized = await wallet.serializeWithEncryptionKey(key, customKdfConfiguration); - expect(JSON.parse(serialized)).toEqual({ - type: "secp256k1wallet-v1", - kdf: customKdfConfiguration, - encryption: { - algorithm: "xchacha20poly1305-ietf", - }, - data: jasmine.stringMatching(base64Matcher), - }); - }); - }); -}); diff --git a/packages/launchpad/src/secp256k1hdwallet.ts b/packages/launchpad/src/secp256k1hdwallet.ts deleted file mode 100644 index 956ad6aa..00000000 --- a/packages/launchpad/src/secp256k1hdwallet.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { - AccountData, - AminoSignResponse, - encodeSecp256k1Signature, - OfflineAminoSigner, - rawSecp256k1PubkeyToRawAddress, - serializeSignDoc, - StdSignDoc, -} from "@cosmjs/amino"; -import { - Bip39, - EnglishMnemonic, - HdPath, - pathToString, - Random, - Secp256k1, - sha256, - Slip10, - Slip10Curve, - stringToPath, -} from "@cosmjs/crypto"; -import { Bech32, fromBase64, fromUtf8, toBase64, toUtf8 } from "@cosmjs/encoding"; -import { assert, isNonNullObject } from "@cosmjs/utils"; - -import { makeCosmoshubPath } from "./paths"; -import { - decrypt, - encrypt, - EncryptionConfiguration, - executeKdf, - KdfConfiguration, - supportedAlgorithms, -} from "./wallet"; - -const serializationTypeV1 = "secp256k1wallet-v1"; - -/** - * A KDF configuration that is not very strong but can be used on the main thread. - * It takes about 1 second in Node.js 12.15 and should have similar runtimes in other modern Wasm hosts. - */ -const basicPasswordHashingOptions: KdfConfiguration = { - algorithm: "argon2id", - params: { - outputLength: 32, - opsLimit: 20, - memLimitKib: 12 * 1024, - }, -}; - -/** - * This interface describes a JSON object holding the encrypted wallet and the meta data. - * All fields in here must be JSON types. - */ -export interface Secp256k1HdWalletSerialization { - /** A format+version identifier for this serialization format */ - readonly type: string; - /** Information about the key derivation function (i.e. password to encryption key) */ - readonly kdf: KdfConfiguration; - /** Information about the symmetric encryption */ - readonly encryption: EncryptionConfiguration; - /** An instance of Secp256k1HdWalletData, which is stringified, encrypted and base64 encoded. */ - readonly data: string; -} - -/** - * Derivation information required to derive a keypair and an address from a mnemonic. - * All fields in here must be JSON types. - */ -interface DerivationInfoJson { - readonly hdPath: string; - readonly prefix: string; -} - -function isDerivationJson(thing: unknown): thing is DerivationInfoJson { - if (!isNonNullObject(thing)) return false; - if (typeof (thing as DerivationInfoJson).hdPath !== "string") return false; - if (typeof (thing as DerivationInfoJson).prefix !== "string") return false; - return true; -} - -/** - * The data of a wallet serialization that is encrypted. - * All fields in here must be JSON types. - */ -interface Secp256k1HdWalletData { - readonly mnemonic: string; - readonly accounts: readonly DerivationInfoJson[]; -} - -function extractKdfConfigurationV1(doc: any): KdfConfiguration { - return doc.kdf; -} - -export function extractKdfConfiguration(serialization: string): KdfConfiguration { - const root = JSON.parse(serialization); - if (!isNonNullObject(root)) throw new Error("Root document is not an object."); - - switch ((root as any).type) { - case serializationTypeV1: - return extractKdfConfigurationV1(root); - default: - throw new Error("Unsupported serialization type"); - } -} - -/** - * Derivation information required to derive a keypair and an address from a mnemonic. - */ -interface DerivationInfo { - readonly hdPath: HdPath; - /** The bech32 address prefix (human readable part). */ - readonly prefix: string; -} - -export class Secp256k1HdWallet implements OfflineAminoSigner { - /** - * Restores a wallet from the given BIP39 mnemonic. - * - * @param mnemonic Any valid English mnemonic. - * @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. - * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". - */ - public static async fromMnemonic( - mnemonic: string, - hdPath: HdPath = makeCosmoshubPath(0), - prefix = "cosmos", - ): Promise { - const mnemonicChecked = new EnglishMnemonic(mnemonic); - const seed = await Bip39.mnemonicToSeed(mnemonicChecked); - const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath); - const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; - return new Secp256k1HdWallet( - mnemonicChecked, - hdPath, - privkey, - Secp256k1.compressPubkey(uncompressed), - prefix, - ); - } - - /** - * Generates a new wallet with a BIP39 mnemonic of the given length. - * - * @param length The number of words in the mnemonic (12, 15, 18, 21 or 24). - * @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. - * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". - */ - public static async generate( - length: 12 | 15 | 18 | 21 | 24 = 12, - hdPath: HdPath = makeCosmoshubPath(0), - prefix = "cosmos", - ): Promise { - const entropyLength = 4 * Math.floor((11 * length) / 33); - const entropy = Random.getBytes(entropyLength); - const mnemonic = Bip39.encode(entropy); - return Secp256k1HdWallet.fromMnemonic(mnemonic.toString(), hdPath, prefix); - } - - /** - * Restores a wallet from an encrypted serialization. - * - * @param password The user provided password used to generate an encryption key via a KDF. - * This is not normalized internally (see "Unicode normalization" to learn more). - */ - public static async deserialize(serialization: string, password: string): Promise { - const root = JSON.parse(serialization); - if (!isNonNullObject(root)) throw new Error("Root document is not an object."); - switch ((root as any).type) { - case serializationTypeV1: - return Secp256k1HdWallet.deserializeTypeV1(serialization, password); - default: - throw new Error("Unsupported serialization type"); - } - } - - /** - * Restores a wallet from an encrypted serialization. - * - * This is an advanced alternative to calling `deserialize(serialization, password)` directly, which allows - * you to offload the KDF execution to a non-UI thread (e.g. in a WebWorker). - * - * The caller is responsible for ensuring the key was derived with the given KDF configuration. This can be - * done using `extractKdfConfiguration(serialization)` and `executeKdf(password, kdfConfiguration)` from this package. - */ - public static async deserializeWithEncryptionKey( - serialization: string, - encryptionKey: Uint8Array, - ): Promise { - const root = JSON.parse(serialization); - if (!isNonNullObject(root)) throw new Error("Root document is not an object."); - const untypedRoot: any = root; - switch (untypedRoot.type) { - case serializationTypeV1: { - const decryptedBytes = await decrypt( - fromBase64(untypedRoot.data), - encryptionKey, - untypedRoot.encryption, - ); - const decryptedDocument = JSON.parse(fromUtf8(decryptedBytes)); - const { mnemonic, accounts } = decryptedDocument; - assert(typeof mnemonic === "string"); - if (!Array.isArray(accounts)) throw new Error("Property 'accounts' is not an array"); - if (accounts.length !== 1) throw new Error("Property 'accounts' only supports one entry"); - const account = accounts[0]; - if (!isDerivationJson(account)) throw new Error("Account is not in the correct format."); - return Secp256k1HdWallet.fromMnemonic(mnemonic, stringToPath(account.hdPath), account.prefix); - } - default: - throw new Error("Unsupported serialization type"); - } - } - - private static async deserializeTypeV1( - serialization: string, - password: string, - ): Promise { - const root = JSON.parse(serialization); - if (!isNonNullObject(root)) throw new Error("Root document is not an object."); - const encryptionKey = await executeKdf(password, (root as any).kdf); - return Secp256k1HdWallet.deserializeWithEncryptionKey(serialization, encryptionKey); - } - - /** Base secret */ - private readonly secret: EnglishMnemonic; - /** Derivation instruction */ - private readonly accounts: readonly DerivationInfo[]; - /** Derived data */ - private readonly pubkey: Uint8Array; - private readonly privkey: Uint8Array; - - protected constructor( - mnemonic: EnglishMnemonic, - hdPath: HdPath, - privkey: Uint8Array, - pubkey: Uint8Array, - prefix: string, - ) { - this.secret = mnemonic; - this.accounts = [ - { - hdPath: hdPath, - prefix: prefix, - }, - ]; - this.privkey = privkey; - this.pubkey = pubkey; - } - - public get mnemonic(): string { - return this.secret.toString(); - } - - private get address(): string { - return Bech32.encode(this.accounts[0].prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey)); - } - - public async getAccounts(): Promise { - return [ - { - algo: "secp256k1", - address: this.address, - pubkey: this.pubkey, - }, - ]; - } - - public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise { - if (signerAddress !== this.address) { - throw new Error(`Address ${signerAddress} not found in wallet`); - } - const message = sha256(serializeSignDoc(signDoc)); - const signature = await Secp256k1.createSignature(message, this.privkey); - const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]); - return { - signed: signDoc, - signature: encodeSecp256k1Signature(this.pubkey, signatureBytes), - }; - } - - /** - * Generates an encrypted serialization of this wallet. - * - * @param password The user provided password used to generate an encryption key via a KDF. - * This is not normalized internally (see "Unicode normalization" to learn more). - */ - public async serialize(password: string): Promise { - const kdfConfiguration = basicPasswordHashingOptions; - const encryptionKey = await executeKdf(password, kdfConfiguration); - return this.serializeWithEncryptionKey(encryptionKey, kdfConfiguration); - } - - /** - * Generates an encrypted serialization of this wallet. - * - * This is an advanced alternative to calling `serialize(password)` directly, which allows you to - * offload the KDF execution to a non-UI thread (e.g. in a WebWorker). - * - * The caller is responsible for ensuring the key was derived with the given KDF options. If this - * is not the case, the wallet cannot be restored with the original password. - */ - public async serializeWithEncryptionKey( - encryptionKey: Uint8Array, - kdfConfiguration: KdfConfiguration, - ): Promise { - const dataToEncrypt: Secp256k1HdWalletData = { - mnemonic: this.mnemonic, - accounts: this.accounts.map( - (account): DerivationInfoJson => ({ - hdPath: pathToString(account.hdPath), - prefix: account.prefix, - }), - ), - }; - const dataToEncryptRaw = toUtf8(JSON.stringify(dataToEncrypt)); - - const encryptionConfiguration: EncryptionConfiguration = { - algorithm: supportedAlgorithms.xchacha20poly1305Ietf, - }; - const encryptedData = await encrypt(dataToEncryptRaw, encryptionKey, encryptionConfiguration); - - const out: Secp256k1HdWalletSerialization = { - type: serializationTypeV1, - kdf: kdfConfiguration, - encryption: encryptionConfiguration, - data: toBase64(encryptedData), - }; - return JSON.stringify(out); - } -} diff --git a/packages/launchpad/src/secp256k1wallet.spec.ts b/packages/launchpad/src/secp256k1wallet.spec.ts deleted file mode 100644 index 57f20687..00000000 --- a/packages/launchpad/src/secp256k1wallet.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { serializeSignDoc, StdSignDoc } from "@cosmjs/amino"; -import { Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto"; -import { fromBase64, fromHex } from "@cosmjs/encoding"; - -import { Secp256k1Wallet } from "./secp256k1wallet"; - -describe("Secp256k1Wallet", () => { - const defaultPrivkey = fromHex("b8c462d2bb0c1a92edf44f735021f16c270f28ee2c3d1cb49943a5e70a3c763e"); - const defaultAddress = "cosmos1kxt5x5q2l57ma2d434pqpafxdm0mgeg9c8cvtx"; - const defaultPubkey = fromHex("03f146c27639179e5b67b8646108f48e1a78b146c74939e34afaa5414ad5c93f8a"); - - describe("fromKey", () => { - it("works", async () => { - const signer = await Secp256k1Wallet.fromKey(defaultPrivkey); - expect(signer).toBeTruthy(); - }); - }); - - describe("getAccounts", () => { - it("resolves to a list of accounts", async () => { - const signer = await Secp256k1Wallet.fromKey(defaultPrivkey); - const accounts = await signer.getAccounts(); - expect(accounts.length).toEqual(1); - expect(accounts[0]).toEqual({ - address: defaultAddress, - algo: "secp256k1", - pubkey: defaultPubkey, - }); - }); - }); - - describe("signAmino", () => { - it("resolves to valid signature", async () => { - const signer = await Secp256k1Wallet.fromKey(defaultPrivkey); - const signDoc: StdSignDoc = { - msgs: [], - fee: { amount: [], gas: "23" }, - chain_id: "foochain", - memo: "hello, world", - account_number: "7", - sequence: "54", - }; - const { signed, signature } = await signer.signAmino(defaultAddress, signDoc); - expect(signed).toEqual(signDoc); - const valid = await Secp256k1.verifySignature( - Secp256k1Signature.fromFixedLength(fromBase64(signature.signature)), - new Sha256(serializeSignDoc(signed)).digest(), - defaultPubkey, - ); - expect(valid).toEqual(true); - }); - }); -}); diff --git a/packages/launchpad/src/secp256k1wallet.ts b/packages/launchpad/src/secp256k1wallet.ts deleted file mode 100644 index 3280189d..00000000 --- a/packages/launchpad/src/secp256k1wallet.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - AccountData, - AminoSignResponse, - encodeSecp256k1Signature, - OfflineAminoSigner, - rawSecp256k1PubkeyToRawAddress, - serializeSignDoc, - StdSignDoc, -} from "@cosmjs/amino"; -import { Secp256k1, Sha256 } from "@cosmjs/crypto"; -import { Bech32 } from "@cosmjs/encoding"; - -/** - * A wallet that holds a single secp256k1 keypair. - * - * If you want to work with BIP39 mnemonics and multiple accounts, use Secp256k1HdWallet. - */ -export class Secp256k1Wallet implements OfflineAminoSigner { - /** - * Creates a Secp256k1Wallet from the given private key - * - * @param privkey The private key. - * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". - */ - public static async fromKey(privkey: Uint8Array, prefix = "cosmos"): Promise { - const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; - return new Secp256k1Wallet(privkey, Secp256k1.compressPubkey(uncompressed), prefix); - } - - private readonly pubkey: Uint8Array; - private readonly privkey: Uint8Array; - private readonly prefix: string; - - private constructor(privkey: Uint8Array, pubkey: Uint8Array, prefix: string) { - this.privkey = privkey; - this.pubkey = pubkey; - this.prefix = prefix; - } - - private get address(): string { - return Bech32.encode(this.prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey)); - } - - public async getAccounts(): Promise { - return [ - { - algo: "secp256k1", - address: this.address, - pubkey: this.pubkey, - }, - ]; - } - - public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise { - if (signerAddress !== this.address) { - throw new Error(`Address ${signerAddress} not found in wallet`); - } - const message = new Sha256(serializeSignDoc(signDoc)).digest(); - const signature = await Secp256k1.createSignature(message, this.privkey); - const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]); - return { - signed: signDoc, - signature: encodeSecp256k1Signature(this.pubkey, signatureBytes), - }; - } -} diff --git a/packages/launchpad/src/signingcosmosclient.spec.ts b/packages/launchpad/src/signingcosmosclient.spec.ts index 12fe1893..4763d583 100644 --- a/packages/launchpad/src/signingcosmosclient.spec.ts +++ b/packages/launchpad/src/signingcosmosclient.spec.ts @@ -1,13 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { Coin } from "@cosmjs/amino"; +import { Coin, makeCosmoshubPath, Secp256k1HdWallet } from "@cosmjs/amino"; import { assert } from "@cosmjs/utils"; import { coin, coins } from "./coins"; import { assertIsBroadcastTxSuccess, PrivateCosmosClient } from "./cosmosclient"; import { GasPrice } from "./fee"; import { MsgDelegate, MsgSend } from "./msgs"; -import { makeCosmoshubPath } from "./paths"; -import { Secp256k1HdWallet } from "./secp256k1hdwallet"; import { PrivateSigningCosmosClient, SigningCosmosClient } from "./signingcosmosclient"; import { base64Matcher, diff --git a/packages/launchpad/src/wallet.ts b/packages/launchpad/src/wallet.ts deleted file mode 100644 index dc85068a..00000000 --- a/packages/launchpad/src/wallet.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - Argon2id, - isArgon2idOptions, - Random, - xchacha20NonceLength, - Xchacha20poly1305Ietf, -} from "@cosmjs/crypto"; -import { toAscii } from "@cosmjs/encoding"; - -/** - * A fixed salt is chosen to archive a deterministic password to key derivation. - * This reduces the scope of a potential rainbow attack to all CosmJS users. - * Must be 16 bytes due to implementation limitations. - */ -export const cosmjsSalt = toAscii("The CosmJS salt."); - -export interface KdfConfiguration { - /** - * An algorithm identifier, such as "argon2id" or "scrypt". - */ - readonly algorithm: string; - /** A map of algorithm-specific parameters */ - readonly params: Record; -} - -export async function executeKdf(password: string, configuration: KdfConfiguration): Promise { - switch (configuration.algorithm) { - case "argon2id": { - const options = configuration.params; - if (!isArgon2idOptions(options)) throw new Error("Invalid format of argon2id params"); - return Argon2id.execute(password, cosmjsSalt, options); - } - default: - throw new Error("Unsupported KDF algorithm"); - } -} - -/** - * Configuration how to encrypt data or how data was encrypted. - * This is stored as part of the wallet serialization and must only contain JSON types. - */ -export interface EncryptionConfiguration { - /** - * An algorithm identifier, such as "xchacha20poly1305-ietf". - */ - readonly algorithm: string; - /** A map of algorithm-specific parameters */ - readonly params?: Record; -} - -export const supportedAlgorithms = { - xchacha20poly1305Ietf: "xchacha20poly1305-ietf", -}; - -export async function encrypt( - plaintext: Uint8Array, - encryptionKey: Uint8Array, - config: EncryptionConfiguration, -): Promise { - switch (config.algorithm) { - case supportedAlgorithms.xchacha20poly1305Ietf: { - const nonce = Random.getBytes(xchacha20NonceLength); - // Prepend fixed-length nonce to ciphertext as suggested in the example from https://github.com/jedisct1/libsodium.js#api - return new Uint8Array([ - ...nonce, - ...(await Xchacha20poly1305Ietf.encrypt(plaintext, encryptionKey, nonce)), - ]); - } - default: - throw new Error(`Unsupported encryption algorithm: '${config.algorithm}'`); - } -} - -export async function decrypt( - ciphertext: Uint8Array, - encryptionKey: Uint8Array, - config: EncryptionConfiguration, -): Promise { - switch (config.algorithm) { - case supportedAlgorithms.xchacha20poly1305Ietf: { - const nonce = ciphertext.slice(0, xchacha20NonceLength); - return Xchacha20poly1305Ietf.decrypt(ciphertext.slice(xchacha20NonceLength), encryptionKey, nonce); - } - default: - throw new Error(`Unsupported encryption algorithm: '${config.algorithm}'`); - } -} From a4bf788ff43e27df7796882c570e24ceb1d13c0a Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 16:47:15 +0100 Subject: [PATCH 06/12] stargate: Use paths/wallets from amino instead of launchpad --- packages/stargate/src/multisignature.spec.ts | 10 ++++++++-- packages/stargate/src/signingstargateclient.spec.ts | 3 ++- packages/stargate/src/testutils.spec.ts | 3 +-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts index d6859597..2ffacf99 100644 --- a/packages/stargate/src/multisignature.spec.ts +++ b/packages/stargate/src/multisignature.spec.ts @@ -1,5 +1,11 @@ -import { createMultisigThresholdPubkey, encodeSecp256k1Pubkey, pubkeyToAddress } from "@cosmjs/amino"; -import { coins, makeCosmoshubPath, Secp256k1HdWallet } from "@cosmjs/launchpad"; +import { + createMultisigThresholdPubkey, + encodeSecp256k1Pubkey, + makeCosmoshubPath, + pubkeyToAddress, + Secp256k1HdWallet, +} from "@cosmjs/amino"; +import { coins } from "@cosmjs/launchpad"; import { assert } from "@cosmjs/utils"; import { MsgSend } from "./codec/cosmos/bank/v1beta1/tx"; diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index e826c554..9a2c96af 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention,no-bitwise */ -import { MsgDelegate as LaunchpadMsgDelegate, Secp256k1HdWallet } from "@cosmjs/launchpad"; +import { Secp256k1HdWallet } from "@cosmjs/amino"; +import { MsgDelegate as LaunchpadMsgDelegate } from "@cosmjs/launchpad"; import { coin, coins, DirectSecp256k1HdWallet, Registry } from "@cosmjs/proto-signing"; import { assert, sleep } from "@cosmjs/utils"; import protobuf from "protobufjs/minimal"; diff --git a/packages/stargate/src/testutils.spec.ts b/packages/stargate/src/testutils.spec.ts index 73d05d63..955345be 100644 --- a/packages/stargate/src/testutils.spec.ts +++ b/packages/stargate/src/testutils.spec.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { AminoSignResponse, StdSignDoc } from "@cosmjs/amino"; +import { AminoSignResponse, Secp256k1HdWallet, StdSignDoc } from "@cosmjs/amino"; import { Bip39, EnglishMnemonic, Random, Secp256k1, Slip10, Slip10Curve } from "@cosmjs/crypto"; import { Bech32 } from "@cosmjs/encoding"; -import { Secp256k1HdWallet } from "@cosmjs/launchpad"; import { coins, DirectSecp256k1HdWallet, From 1d46f271cce965ca1930bc3c269663779cf17057 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 16:48:30 +0100 Subject: [PATCH 07/12] cosmwasm-stargate: Use wallets from amino instead of launchpad --- packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts | 3 ++- packages/cosmwasm-stargate/src/testutils.spec.ts | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts index efc666d1..91da417e 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { Secp256k1HdWallet } from "@cosmjs/amino"; import { UploadMeta } from "@cosmjs/cosmwasm-launchpad"; import { sha256 } from "@cosmjs/crypto"; import { toHex } from "@cosmjs/encoding"; -import { MsgDelegate as LaunchpadMsgDelegate, Secp256k1HdWallet } from "@cosmjs/launchpad"; +import { MsgDelegate as LaunchpadMsgDelegate } from "@cosmjs/launchpad"; import { DirectSecp256k1HdWallet, Registry } from "@cosmjs/proto-signing"; import { AminoTypes, assertIsBroadcastTxSuccess, coin, coins, GasPrice } from "@cosmjs/stargate"; import { DeepPartial, MsgSend } from "@cosmjs/stargate/build/codec/cosmos/bank/v1beta1/tx"; diff --git a/packages/cosmwasm-stargate/src/testutils.spec.ts b/packages/cosmwasm-stargate/src/testutils.spec.ts index 1643309f..95f5c985 100644 --- a/packages/cosmwasm-stargate/src/testutils.spec.ts +++ b/packages/cosmwasm-stargate/src/testutils.spec.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { AminoSignResponse, StdSignDoc } from "@cosmjs/amino"; +import { AminoSignResponse, Secp256k1HdWallet, StdSignDoc } from "@cosmjs/amino"; import { Bip39, EnglishMnemonic, Random, Secp256k1, Slip10, Slip10Curve } from "@cosmjs/crypto"; import { Bech32, fromBase64 } from "@cosmjs/encoding"; -import { Secp256k1HdWallet } from "@cosmjs/launchpad"; import { DirectSecp256k1HdWallet, DirectSignResponse, From 6a2d4718161b843e2689db2031823cc6550572ec Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 17:16:43 +0100 Subject: [PATCH 08/12] stargate: Import coins from proto-signing --- packages/stargate/src/multisignature.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts index 2ffacf99..63e0e945 100644 --- a/packages/stargate/src/multisignature.spec.ts +++ b/packages/stargate/src/multisignature.spec.ts @@ -5,7 +5,7 @@ import { pubkeyToAddress, Secp256k1HdWallet, } from "@cosmjs/amino"; -import { coins } from "@cosmjs/launchpad"; +import { coins } from "@cosmjs/proto-signing"; import { assert } from "@cosmjs/utils"; import { MsgSend } from "./codec/cosmos/bank/v1beta1/tx"; From 6d7f3d279b7b4297c42faef581be17d83b456403 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 18:05:47 +0100 Subject: [PATCH 09/12] amino: Move base64Matcher into testutils --- packages/amino/src/secp256k1hdwallet.spec.ts | 3 +-- packages/amino/src/testutils.spec.ts | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/amino/src/secp256k1hdwallet.spec.ts b/packages/amino/src/secp256k1hdwallet.spec.ts index d852df1f..63a5808e 100644 --- a/packages/amino/src/secp256k1hdwallet.spec.ts +++ b/packages/amino/src/secp256k1hdwallet.spec.ts @@ -4,10 +4,9 @@ import { fromBase64, fromHex } from "@cosmjs/encoding"; import { extractKdfConfiguration, Secp256k1HdWallet } from "./secp256k1hdwallet"; import { serializeSignDoc, StdSignDoc } from "./signdoc"; +import { base64Matcher } from "./testutils.spec"; import { executeKdf, KdfConfiguration } from "./wallet"; -const base64Matcher = /^(?:[a-zA-Z0-9+/]{4})*(?:|(?:[a-zA-Z0-9+/]{3}=)|(?:[a-zA-Z0-9+/]{2}==)|(?:[a-zA-Z0-9+/]{1}===))$/; - describe("Secp256k1HdWallet", () => { // m/44'/118'/0'/0/0 // pubkey: 02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6 diff --git a/packages/amino/src/testutils.spec.ts b/packages/amino/src/testutils.spec.ts index 36cbc26d..18c5f57b 100644 --- a/packages/amino/src/testutils.spec.ts +++ b/packages/amino/src/testutils.spec.ts @@ -1,6 +1,8 @@ import { decodeBech32Pubkey } from "./encoding"; import { MultisigThresholdPubkey } from "./pubkeys"; +export const base64Matcher = /^(?:[a-zA-Z0-9+/]{4})*(?:|(?:[a-zA-Z0-9+/]{3}=)|(?:[a-zA-Z0-9+/]{2}==)|(?:[a-zA-Z0-9+/]{1}===))$/; + // ./build/wasmd keys add test1 // ./build/wasmd keys add test2 // ./build/wasmd keys add test3 From 3db78cd5f77ab0f1dbdd7c3efc355940d01189a1 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 18:07:39 +0100 Subject: [PATCH 10/12] proto-signing: Use paths from amino --- .../src/directsecp256k1hdwallet.ts | 3 +-- packages/proto-signing/src/index.ts | 1 - packages/proto-signing/src/paths.spec.ts | 26 ------------------- packages/proto-signing/src/paths.ts | 17 ------------ 4 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 packages/proto-signing/src/paths.spec.ts delete mode 100644 packages/proto-signing/src/paths.ts diff --git a/packages/proto-signing/src/directsecp256k1hdwallet.ts b/packages/proto-signing/src/directsecp256k1hdwallet.ts index 0a3f02cf..c6c52d89 100644 --- a/packages/proto-signing/src/directsecp256k1hdwallet.ts +++ b/packages/proto-signing/src/directsecp256k1hdwallet.ts @@ -1,4 +1,4 @@ -import { encodeSecp256k1Signature, rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; +import { encodeSecp256k1Signature, makeCosmoshubPath, rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; import { Bip39, EnglishMnemonic, @@ -12,7 +12,6 @@ import { import { Bech32 } from "@cosmjs/encoding"; import { SignDoc } from "./codec/cosmos/tx/v1beta1/tx"; -import { makeCosmoshubPath } from "./paths"; import { AccountData, DirectSignResponse, OfflineDirectSigner } from "./signer"; import { makeSignBytes } from "./signing"; diff --git a/packages/proto-signing/src/index.ts b/packages/proto-signing/src/index.ts index 56518dd2..a27372db 100644 --- a/packages/proto-signing/src/index.ts +++ b/packages/proto-signing/src/index.ts @@ -10,7 +10,6 @@ export { } from "./registry"; export { DirectSecp256k1HdWallet } from "./directsecp256k1hdwallet"; export { DirectSecp256k1Wallet } from "./directsecp256k1wallet"; -export { makeCosmoshubPath } from "./paths"; export { decodePubkey, encodePubkey } from "./pubkey"; export { AccountData, diff --git a/packages/proto-signing/src/paths.spec.ts b/packages/proto-signing/src/paths.spec.ts deleted file mode 100644 index 17fd85b6..00000000 --- a/packages/proto-signing/src/paths.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Slip10RawIndex } from "@cosmjs/crypto"; - -import { makeCosmoshubPath } from "./paths"; - -describe("paths", () => { - describe("makeCosmoshubPath", () => { - it("works", () => { - // m/44'/118'/0'/0/0 - expect(makeCosmoshubPath(0)).toEqual([ - Slip10RawIndex.hardened(44), - Slip10RawIndex.hardened(118), - Slip10RawIndex.hardened(0), - Slip10RawIndex.normal(0), - Slip10RawIndex.normal(0), - ]); - // m/44'/118'/0'/0/123 - expect(makeCosmoshubPath(123)).toEqual([ - Slip10RawIndex.hardened(44), - Slip10RawIndex.hardened(118), - Slip10RawIndex.hardened(0), - Slip10RawIndex.normal(0), - Slip10RawIndex.normal(123), - ]); - }); - }); -}); diff --git a/packages/proto-signing/src/paths.ts b/packages/proto-signing/src/paths.ts deleted file mode 100644 index 1b4f0d8a..00000000 --- a/packages/proto-signing/src/paths.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { HdPath, Slip10RawIndex } from "@cosmjs/crypto"; - -/** - * The Cosmos Hub derivation path in the form `m/44'/118'/0'/0/a` - * with 0-based account index `a`. - * - * This is the same as makeCosmoshubPath from @cosmjs/launchpad but those might diverge in the future. - */ -export function makeCosmoshubPath(a: number): HdPath { - return [ - Slip10RawIndex.hardened(44), - Slip10RawIndex.hardened(118), - Slip10RawIndex.hardened(0), - Slip10RawIndex.normal(0), - Slip10RawIndex.normal(a), - ]; -} From 572346755e4d392b2b0b9709607b69a3db8dbbee Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 18:08:40 +0100 Subject: [PATCH 11/12] stargate: Use paths from amino --- packages/stargate/src/testutils.spec.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/stargate/src/testutils.spec.ts b/packages/stargate/src/testutils.spec.ts index 955345be..9f198f2c 100644 --- a/packages/stargate/src/testutils.spec.ts +++ b/packages/stargate/src/testutils.spec.ts @@ -1,14 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { AminoSignResponse, Secp256k1HdWallet, StdSignDoc } from "@cosmjs/amino"; +import { AminoSignResponse, makeCosmoshubPath, Secp256k1HdWallet, StdSignDoc } from "@cosmjs/amino"; import { Bip39, EnglishMnemonic, Random, Secp256k1, Slip10, Slip10Curve } from "@cosmjs/crypto"; import { Bech32 } from "@cosmjs/encoding"; -import { - coins, - DirectSecp256k1HdWallet, - DirectSignResponse, - makeAuthInfoBytes, - makeCosmoshubPath, -} from "@cosmjs/proto-signing"; +import { coins, DirectSecp256k1HdWallet, DirectSignResponse, makeAuthInfoBytes } from "@cosmjs/proto-signing"; import { SignMode } from "./codec/cosmos/tx/signing/v1beta1/signing"; import { AuthInfo, SignDoc, TxBody } from "./codec/cosmos/tx/v1beta1/tx"; From c043d61565db32c9831e5dbc7a8773446bbf344d Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 18:09:41 +0100 Subject: [PATCH 12/12] cosmwasm-stargate: Use paths from amino --- packages/cosmwasm-stargate/src/testutils.spec.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/cosmwasm-stargate/src/testutils.spec.ts b/packages/cosmwasm-stargate/src/testutils.spec.ts index 95f5c985..9747b2d8 100644 --- a/packages/cosmwasm-stargate/src/testutils.spec.ts +++ b/packages/cosmwasm-stargate/src/testutils.spec.ts @@ -1,13 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { AminoSignResponse, Secp256k1HdWallet, StdSignDoc } from "@cosmjs/amino"; +import { AminoSignResponse, makeCosmoshubPath, Secp256k1HdWallet, StdSignDoc } from "@cosmjs/amino"; import { Bip39, EnglishMnemonic, Random, Secp256k1, Slip10, Slip10Curve } from "@cosmjs/crypto"; import { Bech32, fromBase64 } from "@cosmjs/encoding"; -import { - DirectSecp256k1HdWallet, - DirectSignResponse, - makeAuthInfoBytes, - makeCosmoshubPath, -} from "@cosmjs/proto-signing"; +import { DirectSecp256k1HdWallet, DirectSignResponse, makeAuthInfoBytes } from "@cosmjs/proto-signing"; import { AuthExtension, BankExtension,