mirror of
https://github.com/cosmos/cosmjs.git
synced 2025-03-11 14:09:15 +00:00
launchpad: Use paths/wallets from amino
This commit is contained in:
parent
dde8334f21
commit
edfcc704eb
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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";
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -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),
|
||||
];
|
||||
}
|
@ -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),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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<Secp256k1HdWallet> {
|
||||
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<Secp256k1HdWallet> {
|
||||
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<Secp256k1HdWallet> {
|
||||
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<Secp256k1HdWallet> {
|
||||
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<Secp256k1HdWallet> {
|
||||
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<readonly AccountData[]> {
|
||||
return [
|
||||
{
|
||||
algo: "secp256k1",
|
||||
address: this.address,
|
||||
pubkey: this.pubkey,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise<AminoSignResponse> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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<Secp256k1Wallet> {
|
||||
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<readonly AccountData[]> {
|
||||
return [
|
||||
{
|
||||
algo: "secp256k1",
|
||||
address: this.address,
|
||||
pubkey: this.pubkey,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise<AminoSignResponse> {
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
export async function executeKdf(password: string, configuration: KdfConfiguration): Promise<Uint8Array> {
|
||||
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<string, unknown>;
|
||||
}
|
||||
|
||||
export const supportedAlgorithms = {
|
||||
xchacha20poly1305Ietf: "xchacha20poly1305-ietf",
|
||||
};
|
||||
|
||||
export async function encrypt(
|
||||
plaintext: Uint8Array,
|
||||
encryptionKey: Uint8Array,
|
||||
config: EncryptionConfiguration,
|
||||
): Promise<Uint8Array> {
|
||||
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<Uint8Array> {
|
||||
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}'`);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user