Skip to content

Commit

Permalink
add creation options to fromExtendedKey method
Browse files Browse the repository at this point in the history
  • Loading branch information
arobsn committed Nov 1, 2023
1 parent abc53ff commit e021bfb
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 35 deletions.
55 changes: 47 additions & 8 deletions packages/wallet/src/ergoHDKey.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe("Instantiation", () => {
expect(key.privateKey).not.to.be.empty;

key.wipePrivateData();
expect(key.privateKey).to.be.null;
expect(key.privateKey).to.be.undefined;
});
});

Expand All @@ -61,13 +61,13 @@ describe("Extended keys", () => {
const mnemonic = generateMnemonic();
const key = await ErgoHDKey.fromMnemonic(mnemonic);

expect(key.privateKey).not.to.be.null;
expect(key.publicKey).not.to.be.null;
expect(key.chainCode).not.to.be.null;
expect(key.privateKey).not.to.be.undefined;
expect(key.publicKey).not.to.be.undefined;
expect(key.chainCode).not.to.be.undefined;

const recreatedKeyFromPk = ErgoHDKey.fromExtendedKey(key.extendedPublicKey);

expect(recreatedKeyFromPk.privateKey).to.be.null;
expect(recreatedKeyFromPk.privateKey).to.be.undefined;
expect(recreatedKeyFromPk.publicKey).to.be.deep.equal(key.publicKey);
expect(recreatedKeyFromPk.chainCode).to.be.deep.equal(key.chainCode);
});
Expand All @@ -76,9 +76,9 @@ describe("Extended keys", () => {
const mnemonic = generateMnemonic();
const key = await ErgoHDKey.fromMnemonic(mnemonic);

expect(key.privateKey).not.to.be.null;
expect(key.publicKey).not.to.be.null;
expect(key.chainCode).not.to.be.null;
expect(key.privateKey).not.to.be.undefined;
expect(key.publicKey).not.to.be.undefined;
expect(key.chainCode).not.to.be.undefined;

const recreatedKeyFromPk = ErgoHDKey.fromExtendedKey(key.extendedPrivateKey);

Expand All @@ -87,6 +87,45 @@ describe("Extended keys", () => {
expect(recreatedKeyFromPk.chainCode).to.be.deep.equal(key.chainCode);
});

it("Should create and restore from extended key options method", async () => {
const mnemonic = generateMnemonic();
const key = await ErgoHDKey.fromMnemonic(mnemonic);

expect(key.privateKey).not.to.be.undefined;
expect(key.publicKey).not.to.be.undefined;
expect(key.chainCode).not.to.be.undefined;

const fullRecreatedKey = ErgoHDKey.fromExtendedKey({
depth: key.depth,
index: key.index,
privateKey: key.privateKey!,
chainCode: key.chainCode!
});
expect(fullRecreatedKey.depth).to.be.equal(key.depth);
expect(fullRecreatedKey.index).to.be.equal(key.index);
expect(fullRecreatedKey.privateKey).to.deep.equal(key.privateKey);
expect(fullRecreatedKey.publicKey).to.be.deep.equal(key.publicKey);
expect(fullRecreatedKey.chainCode).to.be.deep.equal(key.chainCode);

const recreatedFromPrivateKey = ErgoHDKey.fromExtendedKey({
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
});
expect(recreatedFromPublicKey.depth).to.be.equal(0);
expect(recreatedFromPublicKey.index).to.be.equal(0);
expect(recreatedFromPublicKey.privateKey).to.be.undefined;
expect(recreatedFromPublicKey.publicKey).to.be.deep.equal(key.publicKey);
expect(recreatedFromPublicKey.chainCode).to.be.undefined;
});

it("Should create from encoded private extended key", async () => {
const encodedXPriv = base58check.encode(
hex.decode(
Expand Down
86 changes: 59 additions & 27 deletions packages/wallet/src/ergoHDKey.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { assert } from "@fleet-sdk/common";
import { ErgoAddress } from "@fleet-sdk/core";
import { HDKey } from "@scure/bip32";
import { mnemonicToSeed, mnemonicToSeedSync } from "@scure/bip39";
Expand All @@ -7,57 +8,77 @@ import { mnemonicToSeed, mnemonicToSeedSync } from "@scure/bip39";
*/
export const ERGO_HD_CHANGE_PATH = "m/44'/429'/0'/0";

const VERSIONS = { private: 0x0488ade4, public: 0x0488b21e };

export type FromMnemonicOptions = {
passphrase?: string;
path?: string;
};

export type HDKeyOptionsBase = {
depth?: number;
index?: number;
parentFingerprint?: number;
chainCode?: Uint8Array;
};

export type PrivateKeyOptions = HDKeyOptionsBase & {
privateKey: Uint8Array | bigint;
};

export type PublicKeyOptions = HDKeyOptionsBase & {
publicKey: Uint8Array;
};

export type HDKeyOptions = PrivateKeyOptions | PublicKeyOptions;

export class ErgoHDKey {
private readonly _root: HDKey;
private readonly _publicKey: Uint8Array;
private readonly _address: ErgoAddress;

private constructor(hdKey: HDKey) {
/* c8 ignore next 3 */
if (!hdKey.publicKey) {
throw new Error("Public key is not present");
}
readonly #root: HDKey;
readonly #publicKey: Uint8Array;

#address?: ErgoAddress;

private constructor(key: HDKey) {
assert(!!key.publicKey, "Public key is not present");

this._root = hdKey;
this._publicKey = hdKey.publicKey;
this._address = ErgoAddress.fromPublicKey(this._publicKey);
this.#root = key;
this.#publicKey = key.publicKey;
}

get publicKey(): Uint8Array {
return this._publicKey;
return this.#publicKey;
}

get privateKey(): Uint8Array | null {
return this._root.privateKey;
get privateKey(): Uint8Array | undefined {
return this.#root.privateKey ?? undefined;
}

get chainCode(): Uint8Array | null {
return this._root.chainCode;
get chainCode(): Uint8Array | undefined {
return this.#root.chainCode ?? undefined;
}

get extendedPublicKey(): string {
return this._root.publicExtendedKey;
return this.#root.publicExtendedKey;
}

get extendedPrivateKey(): string {
return this._root.privateExtendedKey;
return this.#root.privateExtendedKey;
}

get index(): number {
return this._root.index;
return this.#root.index;
}

get depth(): number {
return this._root.depth;
return this.#root.depth;
}

get address(): ErgoAddress {
return this._address;
if (!this.#address) {
this.#address = ErgoAddress.fromPublicKey(this.publicKey);
}

return this.#address;
}

static async fromMnemonic(mnemonic: string, options?: FromMnemonicOptions): Promise<ErgoHDKey> {
Expand All @@ -77,20 +98,31 @@ export class ErgoHDKey {
return new ErgoHDKey(key);
}

static fromExtendedKey(base58EncodedExtKey: string): ErgoHDKey {
return new ErgoHDKey(HDKey.fromExtendedKey(base58EncodedExtKey));
static fromExtendedKey(options: HDKeyOptions): ErgoHDKey;
static fromExtendedKey(encodedKey: string): ErgoHDKey;
static fromExtendedKey(keyOrOptions: string | HDKeyOptions): ErgoHDKey {
const rootKey =
typeof keyOrOptions === "string"
? HDKey.fromExtendedKey(keyOrOptions)
: new HDKey({
versions: VERSIONS,
chainCode: keyOrOptions.chainCode as unknown as Uint8Array,
...keyOrOptions
});

return new ErgoHDKey(rootKey);
}

deriveChild(index: number): ErgoHDKey {
return new ErgoHDKey(this._root.deriveChild(index));
return new ErgoHDKey(this.#root.deriveChild(index));
}

derive(path: string): ErgoHDKey {
return new ErgoHDKey(this._root.derive(path));
return new ErgoHDKey(this.#root.derive(path));
}

wipePrivateData(): ErgoHDKey {
this._root.wipePrivateData();
this.#root.wipePrivateData();

return this;
}
Expand Down

0 comments on commit e021bfb

Please sign in to comment.