From 821b666f98bf8d64a185367bd072e1c3d94f3412 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 8 Jul 2020 16:34:58 +0200 Subject: [PATCH] sdk38: Add Wallet interface and implement for Secp256k1Wallet --- .../sdk38/src/cosmosclient.searchtx.spec.ts | 10 +- packages/sdk38/src/cosmosclient.spec.ts | 6 +- packages/sdk38/src/index.ts | 2 +- packages/sdk38/src/lcdapi/lcdclient.spec.ts | 60 ++++++------ .../sdk38/src/signingcosmosclient.spec.ts | 20 ++-- packages/sdk38/src/wallet.spec.ts | 92 +++++++++++------- packages/sdk38/src/wallet.ts | 93 +++++++++++++------ packages/sdk38/types/index.d.ts | 2 +- packages/sdk38/types/wallet.d.ts | 50 ++++++---- 9 files changed, 207 insertions(+), 128 deletions(-) diff --git a/packages/sdk38/src/cosmosclient.searchtx.spec.ts b/packages/sdk38/src/cosmosclient.searchtx.spec.ts index eaad9abc..fe90b60a 100644 --- a/packages/sdk38/src/cosmosclient.searchtx.spec.ts +++ b/packages/sdk38/src/cosmosclient.searchtx.spec.ts @@ -16,7 +16,7 @@ import { wasmdEnabled, } from "./testutils.spec"; import { CosmosSdkTx } from "./types"; -import { Secp256k1Pen } from "./wallet"; +import { Secp256k1OfflineWallet } from "./wallet"; interface TestTxSend { readonly sender: string; @@ -32,9 +32,9 @@ describe("CosmosClient.searchTx", () => { beforeAll(async () => { if (wasmdEnabled()) { - const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, (signBytes) => - pen.sign(signBytes), + const wallet = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic); + const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, async (signBytes) => + wallet.sign(wallet.address, signBytes), ); { @@ -58,7 +58,7 @@ describe("CosmosClient.searchTx", () => { const { accountNumber, sequence } = await client.getNonce(); const chainId = await client.getChainId(); const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence); - const signature = await pen.sign(signBytes); + const signature = await wallet.sign(wallet.address, signBytes); const tx: CosmosSdkTx = { type: "cosmos-sdk/StdTx", value: { diff --git a/packages/sdk38/src/cosmosclient.spec.ts b/packages/sdk38/src/cosmosclient.spec.ts index e84dc9e1..e56a8fa1 100644 --- a/packages/sdk38/src/cosmosclient.spec.ts +++ b/packages/sdk38/src/cosmosclient.spec.ts @@ -16,7 +16,7 @@ import { wasmd, } from "./testutils.spec"; import { StdFee } from "./types"; -import { Secp256k1Pen } from "./wallet"; +import { Secp256k1OfflineWallet } from "./wallet"; const blockTime = 1_000; // ms @@ -193,7 +193,7 @@ describe("CosmosClient", () => { describe("postTx", () => { it("works", async () => { pendingWithoutWasmd(); - const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const wallet = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic); const client = new CosmosClient(wasmd.endpoint); const memo = "My first contract on chain"; @@ -224,7 +224,7 @@ describe("CosmosClient", () => { const chainId = await client.getChainId(); const { accountNumber, sequence } = await client.getNonce(faucet.address); const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence); - const signature = await pen.sign(signBytes); + const signature = await wallet.sign(wallet.address, signBytes); const signedTx = { msg: [sendMsg], fee: fee, diff --git a/packages/sdk38/src/index.ts b/packages/sdk38/src/index.ts index 9074e9df..5af7b788 100644 --- a/packages/sdk38/src/index.ts +++ b/packages/sdk38/src/index.ts @@ -55,4 +55,4 @@ export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; export { FeeTable, SigningCallback, SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, pubkeyType, CosmosSdkTx, PubKey, StdFee, StdSignature, StdTx } from "./types"; -export { Pen, Secp256k1Pen, makeCosmoshubPath } from "./wallet"; +export { OfflineWallet, Secp256k1OfflineWallet, makeCosmoshubPath } from "./wallet"; diff --git a/packages/sdk38/src/lcdapi/lcdclient.spec.ts b/packages/sdk38/src/lcdapi/lcdclient.spec.ts index d73dfba7..17df4744 100644 --- a/packages/sdk38/src/lcdapi/lcdclient.spec.ts +++ b/packages/sdk38/src/lcdapi/lcdclient.spec.ts @@ -20,7 +20,7 @@ import { wasmdEnabled, } from "../testutils.spec"; import { StdFee } from "../types"; -import { makeCosmoshubPath, Secp256k1Pen } from "../wallet"; +import { makeCosmoshubPath, Secp256k1OfflineWallet } from "../wallet"; import { setupAuthExtension } from "./auth"; import { TxsResponse } from "./base"; import { LcdApiArray, LcdClient, normalizeLcdApiArray } from "./lcdclient"; @@ -217,9 +217,9 @@ describe("LcdClient", () => { beforeAll(async () => { if (wasmdEnabled()) { - const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, (signBytes) => - pen.sign(signBytes), + const wallet = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic); + const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, async (signBytes) => + wallet.sign(wallet.address, signBytes), ); { @@ -267,7 +267,7 @@ describe("LcdClient", () => { const { accountNumber, sequence } = await client.getNonce(); const chainId = await client.getChainId(); const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence); - const signature = await pen.sign(signBytes); + const signature = await wallet.sign(wallet.address, signBytes); const signedTx = { msg: [sendMsg], fee: fee, @@ -351,9 +351,9 @@ describe("LcdClient", () => { beforeAll(async () => { if (wasmdEnabled()) { - const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, (signBytes) => - pen.sign(signBytes), + const wallet = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic); + const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, async (signBytes) => + wallet.sign(wallet.address, signBytes), ); const recipient = makeRandomAddress(); @@ -534,7 +534,7 @@ describe("LcdClient", () => { describe("postTx", () => { it("can send tokens", async () => { pendingWithoutWasmd(); - const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const wallet = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic); const memo = "My first contract on chain"; const theMsg: MsgSend = { @@ -565,7 +565,7 @@ describe("LcdClient", () => { const { account_number, sequence } = (await client.auth.account(faucet.address)).result.value; const signBytes = makeSignBytes([theMsg], fee, wasmd.chainId, memo, account_number, sequence); - const signature = await pen.sign(signBytes); + const signature = await wallet.sign(wallet.address, signBytes); const signedTx = makeSignedTx(theMsg, fee, memo, signature); const result = await client.postTx(signedTx); expect(result.code).toBeUndefined(); @@ -582,9 +582,9 @@ describe("LcdClient", () => { it("can't send transaction with additional signatures", async () => { pendingWithoutWasmd(); - const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); - const account2 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); - const account3 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(2)); + const account1 = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); + const account2 = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); + const account3 = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(2)); const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos"); const address2 = rawSecp256k1PubkeyToAddress(account2.pubkey, "cosmos"); const address3 = rawSecp256k1PubkeyToAddress(account3.pubkey, "cosmos"); @@ -622,9 +622,9 @@ describe("LcdClient", () => { const signBytes1 = makeSignBytes([theMsg], fee, wasmd.chainId, memo, an1, sequence1); const signBytes2 = makeSignBytes([theMsg], fee, wasmd.chainId, memo, an2, sequence2); const signBytes3 = makeSignBytes([theMsg], fee, wasmd.chainId, memo, an3, sequence3); - const signature1 = await account1.sign(signBytes1); - const signature2 = await account2.sign(signBytes2); - const signature3 = await account3.sign(signBytes3); + const signature1 = await account1.sign(account1.address, signBytes1); + const signature2 = await account2.sign(account2.address, signBytes2); + const signature3 = await account3.sign(account3.address, signBytes3); const signedTx = { msg: [theMsg], fee: fee, @@ -638,7 +638,7 @@ describe("LcdClient", () => { it("can send multiple messages with one signature", async () => { pendingWithoutWasmd(); - const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); + const account1 = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos"); const memo = "My first contract on chain"; @@ -683,7 +683,7 @@ describe("LcdClient", () => { const { account_number, sequence } = (await client.auth.account(address1)).result.value; const signBytes = makeSignBytes([msg1, msg2], fee, wasmd.chainId, memo, account_number, sequence); - const signature1 = await account1.sign(signBytes); + const signature1 = await account1.sign(account1.address, signBytes); const signedTx = { msg: [msg1, msg2], fee: fee, @@ -696,8 +696,8 @@ describe("LcdClient", () => { it("can send multiple messages with multiple signatures", async () => { pendingWithoutWasmd(); - const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); - const account2 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); + const account1 = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); + const account2 = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos"); const address2 = rawSecp256k1PubkeyToAddress(account2.pubkey, "cosmos"); @@ -745,8 +745,8 @@ describe("LcdClient", () => { const signBytes1 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an1, sequence1); const signBytes2 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an2, sequence2); - const signature1 = await account1.sign(signBytes1); - const signature2 = await account2.sign(signBytes2); + const signature1 = await account1.sign(account1.address, signBytes1); + const signature2 = await account2.sign(account2.address, signBytes2); const signedTx = { msg: [msg2, msg1], fee: fee, @@ -764,8 +764,8 @@ describe("LcdClient", () => { it("can't send transaction with wrong signature order (1)", async () => { pendingWithoutWasmd(); - const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); - const account2 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); + const account1 = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); + const account2 = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos"); const address2 = rawSecp256k1PubkeyToAddress(account2.pubkey, "cosmos"); @@ -813,8 +813,8 @@ describe("LcdClient", () => { const signBytes1 = makeSignBytes([msg1, msg2], fee, wasmd.chainId, memo, an1, sequence1); const signBytes2 = makeSignBytes([msg1, msg2], fee, wasmd.chainId, memo, an2, sequence2); - const signature1 = await account1.sign(signBytes1); - const signature2 = await account2.sign(signBytes2); + const signature1 = await account1.sign(account1.address, signBytes1); + const signature2 = await account2.sign(account2.address, signBytes2); const signedTx = { msg: [msg1, msg2], fee: fee, @@ -827,8 +827,8 @@ describe("LcdClient", () => { it("can't send transaction with wrong signature order (2)", async () => { pendingWithoutWasmd(); - const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); - const account2 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); + const account1 = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); + const account2 = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos"); const address2 = rawSecp256k1PubkeyToAddress(account2.pubkey, "cosmos"); @@ -876,8 +876,8 @@ describe("LcdClient", () => { const signBytes1 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an1, sequence1); const signBytes2 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an2, sequence2); - const signature1 = await account1.sign(signBytes1); - const signature2 = await account2.sign(signBytes2); + const signature1 = await account1.sign(account1.address, signBytes1); + const signature2 = await account2.sign(account2.address, signBytes2); const signedTx = { msg: [msg2, msg1], fee: fee, diff --git a/packages/sdk38/src/signingcosmosclient.spec.ts b/packages/sdk38/src/signingcosmosclient.spec.ts index dd30c13a..7182cbce 100644 --- a/packages/sdk38/src/signingcosmosclient.spec.ts +++ b/packages/sdk38/src/signingcosmosclient.spec.ts @@ -4,7 +4,7 @@ import { Coin } from "./coins"; import { isPostTxFailure, PrivateCosmWasmClient } from "./cosmosclient"; import { SigningCosmosClient } from "./signingcosmosclient"; import { makeRandomAddress, pendingWithoutWasmd } from "./testutils.spec"; -import { Secp256k1Pen } from "./wallet"; +import { Secp256k1OfflineWallet } from "./wallet"; const httpUrl = "http://localhost:1317"; @@ -21,8 +21,10 @@ const faucet = { describe("SigningCosmosClient", () => { describe("makeReadOnly", () => { it("can be constructed", async () => { - const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmosClient(httpUrl, faucet.address, (signBytes) => pen.sign(signBytes)); + const wallet = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic); + const client = new SigningCosmosClient(httpUrl, faucet.address, async (signBytes) => + wallet.sign(wallet.address, signBytes), + ); expect(client).toBeTruthy(); }); }); @@ -30,8 +32,10 @@ describe("SigningCosmosClient", () => { describe("getHeight", () => { it("always uses authAccount implementation", async () => { pendingWithoutWasmd(); - const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmosClient(httpUrl, faucet.address, (signBytes) => pen.sign(signBytes)); + const wallet = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic); + const client = new SigningCosmosClient(httpUrl, faucet.address, async (signBytes) => + wallet.sign(wallet.address, signBytes), + ); const openedClient = (client as unknown) as PrivateCosmWasmClient; const blockLatestSpy = spyOn(openedClient.lcdClient, "blocksLatest").and.callThrough(); @@ -48,8 +52,10 @@ describe("SigningCosmosClient", () => { describe("sendTokens", () => { it("works", async () => { pendingWithoutWasmd(); - const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmosClient(httpUrl, faucet.address, (signBytes) => pen.sign(signBytes)); + const wallet = await Secp256k1OfflineWallet.fromMnemonic(faucet.mnemonic); + const client = new SigningCosmosClient(httpUrl, faucet.address, async (signBytes) => + wallet.sign(wallet.address, signBytes), + ); // instantiate const transferAmount: readonly Coin[] = [ diff --git a/packages/sdk38/src/wallet.spec.ts b/packages/sdk38/src/wallet.spec.ts index 47bdc354..8d82253f 100644 --- a/packages/sdk38/src/wallet.spec.ts +++ b/packages/sdk38/src/wallet.spec.ts @@ -1,54 +1,74 @@ import { Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto"; -import { fromHex, toAscii } from "@cosmjs/encoding"; +import { fromBase64, fromHex, toAscii } from "@cosmjs/encoding"; -import { decodeSignature } from "./signature"; -import { Secp256k1Pen } from "./wallet"; +import { Secp256k1OfflineWallet } from "./wallet"; + +describe("Secp256k1OfflineWallet", () => { + // 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("Sec256k1Pen", () => { it("can be constructed", async () => { - const pen = await Secp256k1Pen.fromMnemonic( - "zebra slush diet army arrest purpose hawk source west glimpse custom record", - ); - expect(pen).toBeTruthy(); + const wallet = await Secp256k1OfflineWallet.fromMnemonic(defaultMnemonic); + expect(wallet).toBeTruthy(); }); - describe("pubkey", () => { - it("returns compressed pubkey", async () => { - // special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling - // m/44'/118'/0'/0/0 - // pubkey: 02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6 - const pen = await Secp256k1Pen.fromMnemonic( - "special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling", - ); - expect(pen.pubkey).toEqual( - fromHex("02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6"), + describe("enable", () => { + it("resolves to true", async () => { + const wallet = await Secp256k1OfflineWallet.fromMnemonic(defaultMnemonic); + const enabled = await wallet.enable(); + expect(enabled).toEqual(true); + }); + }); + + describe("getAccounts", () => { + it("rejects if not enabled", async () => { + const wallet = await Secp256k1OfflineWallet.fromMnemonic(defaultMnemonic); + await expectAsync(wallet.getAccounts()).toBeRejectedWithError(/wallet not enabled/i); + }); + + it("resolves to a list of accounts if enabled", async () => { + const wallet = await Secp256k1OfflineWallet.fromMnemonic(defaultMnemonic); + await wallet.enable(); + 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 Secp256k1OfflineWallet.fromMnemonic( + "oyster design unusual machine spread century engine gravity focus cave carry slot", ); + await wallet.enable(); + const { address } = (await wallet.getAccounts())[0]; + expect(address).toEqual("cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u"); }); }); describe("sign", () => { - it("creates correct signatures", async () => { - const pen = await Secp256k1Pen.fromMnemonic( - "special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling", - ); - const data = toAscii("foo bar"); - const { pubkey, signature } = decodeSignature(await pen.sign(data)); + it("rejects if not enabled", async () => { + const wallet = await Secp256k1OfflineWallet.fromMnemonic(defaultMnemonic); + const message = toAscii("foo bar"); + await expectAsync(wallet.sign(defaultAddress, message)).toBeRejectedWithError(/wallet not enabled/i); + }); + it("resolves to valid signature if enabled", async () => { + const wallet = await Secp256k1OfflineWallet.fromMnemonic(defaultMnemonic); + await wallet.enable(); + const message = toAscii("foo bar"); + const signature = await wallet.sign(defaultAddress, message); const valid = await Secp256k1.verifySignature( - Secp256k1Signature.fromFixedLength(signature), - new Sha256(data).digest(), - pubkey, + Secp256k1Signature.fromFixedLength(fromBase64(signature.signature)), + new Sha256(message).digest(), + defaultPubkey, ); expect(valid).toEqual(true); }); }); - - describe("address", () => { - it("creates same address as Go imlementation", async () => { - const pen = await Secp256k1Pen.fromMnemonic( - "oyster design unusual machine spread century engine gravity focus cave carry slot", - ); - expect(pen.address("cosmos")).toEqual("cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u"); - }); - }); }); diff --git a/packages/sdk38/src/wallet.ts b/packages/sdk38/src/wallet.ts index 1c65a0da..92365e79 100644 --- a/packages/sdk38/src/wallet.ts +++ b/packages/sdk38/src/wallet.ts @@ -15,19 +15,30 @@ import { StdSignature } from "./types"; export type PrehashType = "sha256" | "sha512" | null; -/** - * A pen is the most basic tool you can think of for signing. It works - * everywhere and can be used intuitively by everyone. However, it does not - * come with a great amount of features. End of semi suitable metaphor. - * - * This wraps a single keypair and allows for signing. - * - * Non-goals of this types are: multi account support, persistency, data migrations, - * obfuscation of sensitive data. - */ -export interface Pen { +export type Algo = "secp256k1" | "ed25519" | "sr25519"; + +export interface AccountData { + // bech32-encoded + readonly address: string; + readonly algo: Algo; readonly pubkey: Uint8Array; - readonly sign: (signBytes: Uint8Array, prehashType?: PrehashType) => Promise; +} + +export interface OfflineWallet { + /** + * Request access to the user's accounts. Wallet should ask the user to approve or deny access. Returns true if granted access or false if denied. + */ + readonly enable: () => Promise; + + /** + * Get AccountData array from wallet. Rejects if not enabled. + */ + readonly getAccounts: () => Promise; + + /** + * Request signature from whichever key corresponds to provided bech32-encoded address. Rejects if not enabled. + */ + readonly sign: (address: string, message: Uint8Array, prehashType?: PrehashType) => Promise; } function prehash(bytes: Uint8Array, type: PrehashType): Uint8Array { @@ -57,36 +68,66 @@ export function makeCosmoshubPath(a: number): readonly Slip10RawIndex[] { ]; } -export class Secp256k1Pen implements Pen { +export class Secp256k1OfflineWallet implements OfflineWallet { public static async fromMnemonic( mnemonic: string, hdPath: readonly Slip10RawIndex[] = makeCosmoshubPath(0), - ): Promise { + prefix = "cosmos", + ): Promise { const seed = await Bip39.mnemonicToSeed(new EnglishMnemonic(mnemonic)); const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath); const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; - return new Secp256k1Pen(privkey, Secp256k1.compressPubkey(uncompressed)); + return new Secp256k1OfflineWallet(privkey, Secp256k1.compressPubkey(uncompressed), prefix); } public readonly pubkey: Uint8Array; private readonly privkey: Uint8Array; + private readonly prefix: string; + private readonly algo: Algo = "secp256k1"; + private enabled = false; - private constructor(privkey: Uint8Array, pubkey: Uint8Array) { + private constructor(privkey: Uint8Array, pubkey: Uint8Array, prefix: string) { this.privkey = privkey; this.pubkey = pubkey; + this.prefix = prefix; } - /** - * Creates and returns a signature - */ - public async sign(signBytes: Uint8Array, prehashType: PrehashType = "sha256"): Promise { - const message = prehash(signBytes, prehashType); - const signature = await Secp256k1.createSignature(message, this.privkey); - const fixedLengthSignature = new Uint8Array([...signature.r(32), ...signature.s(32)]); - return encodeSecp256k1Signature(this.pubkey, fixedLengthSignature); + public get address(): string { + return rawSecp256k1PubkeyToAddress(this.pubkey, this.prefix); } - public address(prefix: string): string { - return rawSecp256k1PubkeyToAddress(this.pubkey, prefix); + public async enable(): Promise { + this.enabled = true; + return this.enabled; + } + + public async getAccounts(): Promise { + if (!this.enabled) { + throw new Error("Wallet not enabled"); + } + return [ + { + address: this.address, + algo: this.algo, + pubkey: this.pubkey, + }, + ]; + } + + public async sign( + address: string, + message: Uint8Array, + prehashType: PrehashType = "sha256", + ): Promise { + if (!this.enabled) { + throw new Error("Wallet not enabled"); + } + if (address !== this.address) { + throw new Error(`Address ${address} not found in wallet`); + } + const hashedMessage = prehash(message, prehashType); + const signature = await Secp256k1.createSignature(hashedMessage, this.privkey); + const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]); + return encodeSecp256k1Signature(this.pubkey, signatureBytes); } } diff --git a/packages/sdk38/types/index.d.ts b/packages/sdk38/types/index.d.ts index 8210230f..76c332f0 100644 --- a/packages/sdk38/types/index.d.ts +++ b/packages/sdk38/types/index.d.ts @@ -53,4 +53,4 @@ export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; export { FeeTable, SigningCallback, SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, pubkeyType, CosmosSdkTx, PubKey, StdFee, StdSignature, StdTx } from "./types"; -export { Pen, Secp256k1Pen, makeCosmoshubPath } from "./wallet"; +export { OfflineWallet, Secp256k1OfflineWallet, makeCosmoshubPath } from "./wallet"; diff --git a/packages/sdk38/types/wallet.d.ts b/packages/sdk38/types/wallet.d.ts index 39e899f4..16d837e5 100644 --- a/packages/sdk38/types/wallet.d.ts +++ b/packages/sdk38/types/wallet.d.ts @@ -1,33 +1,45 @@ import { Slip10RawIndex } from "@cosmjs/crypto"; import { StdSignature } from "./types"; export declare type PrehashType = "sha256" | "sha512" | null; -/** - * A pen is the most basic tool you can think of for signing. It works - * everywhere and can be used intuitively by everyone. However, it does not - * come with a great amount of features. End of semi suitable metaphor. - * - * This wraps a single keypair and allows for signing. - * - * Non-goals of this types are: multi account support, persistency, data migrations, - * obfuscation of sensitive data. - */ -export interface Pen { +export declare type Algo = "secp256k1" | "ed25519" | "sr25519"; +export interface AccountData { + readonly address: string; + readonly algo: Algo; readonly pubkey: Uint8Array; - readonly sign: (signBytes: Uint8Array, prehashType?: PrehashType) => Promise; +} +export interface OfflineWallet { + /** + * Request access to the user's accounts. Wallet should ask the user to approve or deny access. Returns true if granted access or false if denied. + */ + readonly enable: () => Promise; + /** + * Get AccountData array from wallet. Rejects if not enabled. + */ + readonly getAccounts: () => Promise; + /** + * Request signature from whichever key corresponds to provided bech32-encoded address. Rejects if not enabled. + */ + readonly sign: (address: string, message: Uint8Array, prehashType?: PrehashType) => Promise; } /** * The Cosmoshub derivation path in the form `m/44'/118'/0'/0/a` * with 0-based account index `a`. */ export declare function makeCosmoshubPath(a: number): readonly Slip10RawIndex[]; -export declare class Secp256k1Pen implements Pen { - static fromMnemonic(mnemonic: string, hdPath?: readonly Slip10RawIndex[]): Promise; +export declare class Secp256k1OfflineWallet implements OfflineWallet { + static fromMnemonic( + mnemonic: string, + hdPath?: readonly Slip10RawIndex[], + prefix?: string, + ): Promise; readonly pubkey: Uint8Array; private readonly privkey; + private readonly prefix; + private readonly algo; + private enabled; private constructor(); - /** - * Creates and returns a signature - */ - sign(signBytes: Uint8Array, prehashType?: PrehashType): Promise; - address(prefix: string): string; + get address(): string; + enable(): Promise; + getAccounts(): Promise; + sign(address: string, message: Uint8Array, prehashType?: PrehashType): Promise; }