diff --git a/.changeset/angry-bobcats-poke.md b/.changeset/angry-bobcats-poke.md index 221223df..837621b1 100644 --- a/.changeset/angry-bobcats-poke.md +++ b/.changeset/angry-bobcats-poke.md @@ -2,4 +2,4 @@ "@fleet-sdk/wallet": minor --- -Add `ProveDLog` transaction signing and verifying +Add `ProveDLog` transaction signing and verification diff --git a/.changeset/friendly-monkeys-do.md b/.changeset/friendly-monkeys-do.md new file mode 100644 index 00000000..a9316ba1 --- /dev/null +++ b/.changeset/friendly-monkeys-do.md @@ -0,0 +1,5 @@ +--- +"@fleet-sdk/wallet": minor +--- + +Add hex encoding support to `ErgoHDKey#fromExtendedKey()` diff --git a/.changeset/itchy-games-switch.md b/.changeset/itchy-games-switch.md new file mode 100644 index 00000000..2a883d45 --- /dev/null +++ b/.changeset/itchy-games-switch.md @@ -0,0 +1,5 @@ +--- +"@fleet-sdk/wallet": minor +--- + +Added `ErgoHDKey#isNeutered()`, `ErgoHDKey#isExtended()`, and `ErgoHDKey#hasPrivateKey()` utility methods. diff --git a/.changeset/sour-pillows-drum.md b/.changeset/sour-pillows-drum.md new file mode 100644 index 00000000..40e24e2c --- /dev/null +++ b/.changeset/sour-pillows-drum.md @@ -0,0 +1,5 @@ +--- +"@fleet-sdk/wallet": minor +--- + +Deprecate optioned arguments for `ErgoHDKey#fromExtendedKeys()` and create default constructor diff --git a/packages/common/src/utils/bytes.bench.ts b/packages/common/src/utils/bytes.bench.ts index 5bc6bc16..bfd0e266 100644 --- a/packages/common/src/utils/bytes.bench.ts +++ b/packages/common/src/utils/bytes.bench.ts @@ -12,11 +12,9 @@ const HEX_CHARSET = new Set("0123456789abcdefABCDEF"); function isHexChar(value: string) { if (!value || value.length % 2) return false; - const valueSet = new Set(Array.from(value)); + const valueSet = new Set(value); for (const c of valueSet) { - if (!HEX_CHARSET.has(c)) { - return false; - } + if (!HEX_CHARSET.has(c)) return false; } return true; diff --git a/packages/wallet/src/ergoHDKey.spec.ts b/packages/wallet/src/ergoHDKey.spec.ts index 1323dbcd..f180f7a7 100644 --- a/packages/wallet/src/ergoHDKey.spec.ts +++ b/packages/wallet/src/ergoHDKey.spec.ts @@ -2,7 +2,7 @@ import { first } from "@fleet-sdk/common"; import { base58check, hex } from "@fleet-sdk/crypto"; import { mnemonicToSeedSync } from "@scure/bip39"; import SigmaRust from "ergo-lib-wasm-nodejs"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, test } from "vitest"; import { keyAddressesTestVectors } from "./_test-vectors/keyVectors"; import { ERGO_CHANGE_PATH, ErgoHDKey } from "./ergoHDKey"; import { generateMnemonic } from "./mnemonic"; @@ -53,10 +53,27 @@ describe("Instantiation", () => { key.wipePrivateData(); expect(key.privateKey).to.be.undefined; }); + + test("Utility checks", () => { + const fullKey = ErgoHDKey.fromMnemonicSync(generateMnemonic()); + expect(fullKey.hasPrivateKey()).to.be.true; + expect(fullKey.isNeutered()).to.be.false; + expect(fullKey.isExtended()).to.be.true; + + const neuteredKey = fullKey.wipePrivateData(); + expect(neuteredKey.hasPrivateKey()).to.be.false; + expect(neuteredKey.isNeutered()).to.be.true; + expect(neuteredKey.isExtended()).to.be.true; + + const simplePk = new ErgoHDKey({ publicKey: fullKey.publicKey }); + expect(simplePk.hasPrivateKey()).to.be.false; + expect(simplePk.isNeutered()).to.be.true; + expect(simplePk.isExtended()).to.be.false; + }); }); describe("Extended keys", () => { - it("Should create and restore from public extended key", async () => { + it("Should create and restore from public encoded extended key", async () => { const mnemonic = generateMnemonic(); const key = await ErgoHDKey.fromMnemonic(mnemonic); @@ -71,7 +88,15 @@ describe("Extended keys", () => { expect(recreatedKeyFromPk.chainCode).to.be.deep.equal(key.chainCode); }); - it("Should create and restore from private extended key", async () => { + it("Should create from extended public key encoded as hex string", () => { + const xpk = + "0488b21e000000000000000000412bc02c85a3499d85f985ee6068c23f2345eb1c8b3fc34309df13b6a5ac56e803cf03e43683a9b7965603a5cc222bd31fa269d12527cda358965716c794c82253"; + + const key = ErgoHDKey.fromExtendedKey(xpk).deriveChild(0); + expect(key.address.encode()).to.be.equal("9gfJXuRPGwocBsVvKc6GPp294Y3i24v3rd5GP6UXQiV1bjChoDB"); + }); + + it("Should create and restore from private encoded extended key", async () => { const mnemonic = generateMnemonic(); const key = await ErgoHDKey.fromMnemonic(mnemonic); @@ -106,18 +131,14 @@ describe("Extended keys", () => { expect(fullyRecreatedKey.publicKey).to.be.deep.equal(key.publicKey); expect(fullyRecreatedKey.chainCode).to.be.deep.equal(key.chainCode); - const recreatedFromPrivateKey = ErgoHDKey.fromExtendedKey({ - privateKey: key.privateKey! - }); + const recreatedFromPrivateKey = new ErgoHDKey({ privateKey: key.privateKey! }); expect(recreatedFromPrivateKey.depth).to.be.equal(0); expect(recreatedFromPrivateKey.index).to.be.equal(0); expect(recreatedFromPrivateKey.privateKey).to.deep.equal(key.privateKey); expect(recreatedFromPrivateKey.publicKey).to.be.deep.equal(key.publicKey); expect(recreatedFromPrivateKey.chainCode).to.be.undefined; - const recreatedFromPublicKey = ErgoHDKey.fromExtendedKey({ - publicKey: key.publicKey - }); + const recreatedFromPublicKey = new ErgoHDKey({ publicKey: key.publicKey }); expect(recreatedFromPublicKey.depth).to.be.equal(0); expect(recreatedFromPublicKey.index).to.be.equal(0); expect(recreatedFromPublicKey.privateKey).to.be.undefined; diff --git a/packages/wallet/src/ergoHDKey.ts b/packages/wallet/src/ergoHDKey.ts index 5ebd7d74..706c046a 100644 --- a/packages/wallet/src/ergoHDKey.ts +++ b/packages/wallet/src/ergoHDKey.ts @@ -1,5 +1,6 @@ -import { assert } from "@fleet-sdk/common"; +import { isHex } from "@fleet-sdk/common"; import { ErgoAddress } from "@fleet-sdk/core"; +import { base58check, hex } from "@fleet-sdk/crypto"; import { HDKey } from "@scure/bip32"; import { mnemonicToSeed, mnemonicToSeedSync } from "@scure/bip39"; @@ -28,21 +29,20 @@ export type PublicKeyOptions = HDKeyOptions & { publicKey: Uint8Array; }; +type ExtendedErgoKey = ErgoHDKey & { chainCode: Uint8Array }; +type FullErgoKey = ExtendedErgoKey & { chainCode: Uint8Array; privateKey: Uint8Array }; +type NeuteredErgoKey = Omit; + export class ErgoHDKey { readonly #root: HDKey; - readonly #publicKey: Uint8Array; - #address?: ErgoAddress; - private constructor(key: HDKey) { - assert(!!key.publicKey, "Public key is not present"); - - this.#root = key; - this.#publicKey = key.publicKey; + constructor(keyOrOpt: HDKey | PrivateKeyOptions | PublicKeyOptions) { + this.#root = keyOrOpt instanceof HDKey ? keyOrOpt : new HDKey(keyOrOpt); } get publicKey(): Uint8Array { - return this.#publicKey; + return this.#root.publicKey!; } get privateKey(): Uint8Array | undefined { @@ -92,16 +92,23 @@ export class ErgoHDKey { return new ErgoHDKey(key); } + /** + * Create an ErgoHDKey from an extended key + * @param encodedKey + */ + static fromExtendedKey(encodedKey: string): ErgoHDKey; + /** @deprecated use the default constructor instead */ static fromExtendedKey(options: PrivateKeyOptions): ErgoHDKey; + /** @deprecated use the default constructor instead */ static fromExtendedKey(options: PublicKeyOptions): ErgoHDKey; - static fromExtendedKey(encodedKey: string): ErgoHDKey; + /** @deprecated use the default constructor instead */ static fromExtendedKey(keyOrOptions: string | PrivateKeyOptions | PublicKeyOptions): ErgoHDKey { - const rootKey = - typeof keyOrOptions === "string" - ? HDKey.fromExtendedKey(keyOrOptions) - : new HDKey(keyOrOptions); + if (typeof keyOrOptions !== "string") { + return new ErgoHDKey(keyOrOptions); + } - return new ErgoHDKey(rootKey); + const xKey = isHex(keyOrOptions) ? base58check.encode(hex.decode(keyOrOptions)) : keyOrOptions; + return new ErgoHDKey(HDKey.fromExtendedKey(xKey)); } deriveChild(index: number): ErgoHDKey { @@ -112,9 +119,20 @@ export class ErgoHDKey { return new ErgoHDKey(this.#root.derive(path)); } - wipePrivateData(): ErgoHDKey { + wipePrivateData(): NeuteredErgoKey { this.#root.wipePrivateData(); - return this; } + + isExtended(): this is ExtendedErgoKey { + return this.chainCode !== undefined; + } + + isNeutered(): this is NeuteredErgoKey { + return this.privateKey === undefined; + } + + hasPrivateKey(): this is FullErgoKey { + return this.privateKey !== undefined; + } }