Skip to content

Commit

Permalink
feat(wallet): add ErgoHDKey for key handling
Browse files Browse the repository at this point in the history
  • Loading branch information
capt-nemo429 committed May 1, 2023
1 parent 43540c1 commit 22aac26
Show file tree
Hide file tree
Showing 16 changed files with 495 additions and 2 deletions.
1 change: 1 addition & 0 deletions .versionrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"./package.json",
"./packages/core/package.json",
"./packages/common/package.json",
"./packages/wallet/package.json",
"./plugins/babel-fees/package.json"
]
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This is a [monorepository](https://monorepo.tools/) which means this contains ma
| ------------------------------------- | -------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------: |
| [@fleet-sdk/core](/packages/core/) | Core library with transaction builder and basic serialization. | [![npm](https://badgen.net/npm/v/@fleet-sdk/core)](https://www.npmjs.com/package/@fleet-sdk/core) |
| [@fleet-sdk/common](/packages/common) | Internal utility functions, constants and types shared across @fleet-sdk packages. | [![npm](https://badgen.net/npm/v/@fleet-sdk/common)](https://www.npmjs.com/package/@fleet-sdk/common) |
| @fleet-sdk/wallet | Wallet related library, with wallet creation, derivation and signing. | `planned` |
| [@fleet-sdk/wallet](/packages/wallet) | Wallet related library, with keys creation, derivation and signing. | [![npm](https://badgen.net/npm/v/@fleet-sdk/wallet)](https://www.npmjs.com/package/@fleet-sdk/wallet) |
| @fleet-sdk/interpreter | Sigma state interpreter and serialization library powered by Sigma.JS. | `planned` |
| @fleet-sdk/compiler | ErgoScript compiler library powered by Sigma.JS. | `planned` |
| @fleet-sdk/graphql-client | Data client library for [ergo-graphql](https://github.com/capt-nemo429/ergo-graphql) server. | `planned` |
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"devDependencies": {
"@noble/hashes": "^1.3.0",
"@scure/base": "^1.1.1",
"@scure/bip32": "^1.3.0",
"@scure/bip39": "^1.2.0",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
Expand All @@ -47,5 +49,8 @@
"engines": {
"node": ">=10",
"pnpm": ">=8.1.1 <9"
},
"dependencies": {
"ergo-lib-wasm-nodejs": "^0.23.0"
}
}
21 changes: 21 additions & 0 deletions packages/wallet/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 capt-nemo429

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 3 additions & 0 deletions packages/wallet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @fleet-sdk/wallet [![License](https://badgen.net/github/license/fleet-sdk/fleet/)](https://github.com/fleet-sdk/fleet/blob/master/LICENSE) [![npm](https://badgen.net/npm/v/@fleet-sdk/wallet)](https://www.npmjs.com/package/@fleet-sdk/wallet)

Key management and signature for Ergo Platform.
49 changes: 49 additions & 0 deletions packages/wallet/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@fleet-sdk/wallet",
"version": "0.1.0-alpha.26",
"description": "Key management and signature for Ergo Platform.",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"typings": "dist/esm/index.d.ts",
"sideEffects": false,
"repository": {
"type": "git",
"url": "https://github.com/fleet-sdk/fleet.git",
"directory": "packages/wallet"
},
"license": "MIT",
"publishConfig": {
"access": "public"
},
"keywords": [
"ergo",
"blockchain",
"transactions",
"serialization"
],
"scripts": {
"build": "run-p build:*",
"build:cjs": "tsc -p tsconfig.json",
"build:esm": "tsc -p tsconfig.esm.json",
"watch:build": "tsc -p tsconfig.json -w",
"watch:unit": "vitest"
},
"dependencies": {
"@fleet-sdk/core": "workspace:^",
"@scure/bip32": "^1.3.0",
"@scure/bip39": "^1.2.0"
},
"engines": {
"node": ">=10"
},
"files": [
"src",
"dist",
"!**/*.spec.*",
"!**/*.json",
"!tests",
"CHANGELOG.md",
"LICENSE",
"README.md"
]
}
157 changes: 157 additions & 0 deletions packages/wallet/src/ergoHDKey.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { first } from "@fleet-sdk/common";
import { mnemonicToSeedSync } from "@scure/bip39";
import SigmaRust from "ergo-lib-wasm-nodejs";
import { describe, expect, it } from "vitest";
import { ERGO_HD_CHANGE_PATH, ErgoHDKey } from "./ergoHDKey";
import { generateMnemonic } from "./mnemonic";
import { keyAddressesTestVectors } from "./tests/keyTestVectors";

describe("Instantiation", () => {
it("Should create from mnemonic and auto derive to ergo's default change path by default", async () => {
const mnemonic = generateMnemonic();
const key = await ErgoHDKey.fromMnemonic(mnemonic);

expect(key.publicKey).not.to.be.empty;
expect(key.privateKey).not.to.be.empty;
expect(key.chainCode).not.to.be.empty;
expect(key.index).to.be.equal(0);
expect(key.depth).to.be.equal(4, "should automatically derive Ergo path.");

const syncKey = ErgoHDKey.fromMnemonicSync(mnemonic);
expect(key).to.be.deep.equal(syncKey);
});

it("Should create from master seed and derive to ergo's default change path by default", () => {
const seed = mnemonicToSeedSync(generateMnemonic());
const key = ErgoHDKey.fromMasterSeed(seed);

expect(key.publicKey).not.to.be.empty;
expect(key.privateKey).not.to.be.empty;
expect(key.chainCode).not.to.be.empty;
expect(key.index).to.be.equal(0);
expect(key.depth).to.be.equal(4, "should automatically derive Ergo path.");
expect(first(key.address.getPublicKeys())).to.be.deep.equal(key.publicKey);
});

it("Should should result in the same key from seed and from equivalent mnemonic", () => {
const mnemonic = generateMnemonic();
const seed = mnemonicToSeedSync(mnemonic);
const keyFromSeed = ErgoHDKey.fromMasterSeed(seed);
const keyFromMnemonic = ErgoHDKey.fromMnemonicSync(mnemonic);

expect(keyFromSeed.publicKey).to.be.deep.equal(keyFromMnemonic.publicKey);
expect(keyFromSeed.privateKey).to.be.deep.equal(keyFromMnemonic.privateKey);
expect(keyFromSeed.chainCode).to.be.deep.equal(keyFromMnemonic.chainCode);
expect(keyFromSeed.index).to.be.equal(keyFromMnemonic.index);
expect(keyFromSeed.depth).to.be.equal(keyFromMnemonic.depth);
});

it("Should wipe private data", () => {
const key = ErgoHDKey.fromMnemonicSync(generateMnemonic());
expect(key.privateKey).not.to.be.empty;

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

describe("Extended keys", () => {
it("Should create and restore from public extended key", async () => {
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;

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

expect(recreatedKeyFromPk.privateKey).to.be.null;
expect(recreatedKeyFromPk.publicKey).to.be.deep.equal(key.publicKey);
expect(recreatedKeyFromPk.chainCode).to.be.deep.equal(key.chainCode);
});

it("Should create and restore from private extended key", async () => {
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;

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

expect(recreatedKeyFromPk.privateKey).to.deep.equal(key.privateKey);
expect(recreatedKeyFromPk.publicKey).to.be.deep.equal(key.publicKey);
expect(recreatedKeyFromPk.chainCode).to.be.deep.equal(key.chainCode);
});
});

describe("Key derivation", () => {
it("Should derive and encode the correct addresses from mnemonic", async () => {
for (const tv of keyAddressesTestVectors) {
const key = await ErgoHDKey.fromMnemonic(tv.mnemonic);

for (let i = 0; i < keyAddressesTestVectors.length; i++) {
expect(key.deriveChild(i).address.encode()).to.be.equal(tv.addresses[i]);
}
}
});

it("Should not auto derive to default Ergo path if path == ''", async () => {
for (const tv of keyAddressesTestVectors) {
let key = await ErgoHDKey.fromMnemonic(tv.mnemonic, { path: "" });
expect(key.depth).to.be.equal(0);
expect(key.index).to.be.equal(0);

key = key.derive(ERGO_HD_CHANGE_PATH);
expect(key.depth).to.be.equal(4);
expect(key.index).to.be.equal(0);

for (let i = 0; i < keyAddressesTestVectors.length; i++) {
expect(key.deriveChild(i).address.encode()).to.be.equal(tv.addresses[i]);
}
}
});

it("Should be on pair with sigma-rust with no passphrase", () => {
const mnemonic = generateMnemonic();

const fleetKey = ErgoHDKey.fromMnemonicSync(mnemonic);
const wasmKey = SigmaRust.ExtSecretKey.derive_master(mnemonicToSeedSync(mnemonic)).derive(
SigmaRust.DerivationPath.from_string(ERGO_HD_CHANGE_PATH)
);

expect(fleetKey.publicKey).to.be.deep.equal(wasmKey.public_key().pub_key_bytes());
expect(fleetKey.privateKey).to.be.deep.equal(wasmKey.secret_key_bytes());

for (let i = 0; i < 100; i++) {
const fleetChild = fleetKey.deriveChild(i);
const wasmChild = wasmKey.child(i.toString());

expect(fleetChild.publicKey).to.be.deep.equal(wasmChild.public_key().pub_key_bytes());
expect(fleetChild.privateKey).to.be.deep.equal(wasmChild.secret_key_bytes());
}
});

it("Should be on pair with sigma-rust with passphrase", () => {
const mnemonic = generateMnemonic();
const passphrase = "test passphrase";

const fleetKey = ErgoHDKey.fromMnemonicSync(mnemonic, { passphrase });
const wasmKey = SigmaRust.ExtSecretKey.derive_master(
SigmaRust.Mnemonic.to_seed(mnemonic, passphrase)
).derive(SigmaRust.DerivationPath.from_string(ERGO_HD_CHANGE_PATH));

expect(fleetKey.publicKey).to.be.deep.equal(wasmKey.public_key().pub_key_bytes());
expect(fleetKey.privateKey).to.be.deep.equal(wasmKey.secret_key_bytes());

for (let i = 0; i < 100; i++) {
const fleetChild = fleetKey.deriveChild(i);
const wasmChild = wasmKey.child(i.toString());

expect(fleetChild.publicKey).to.be.deep.equal(wasmChild.public_key().pub_key_bytes());
expect(fleetChild.privateKey).to.be.deep.equal(wasmChild.secret_key_bytes());
}
});
});
97 changes: 97 additions & 0 deletions packages/wallet/src/ergoHDKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { ErgoAddress } from "@fleet-sdk/core";
import { HDKey } from "@scure/bip32";
import { mnemonicToSeed, mnemonicToSeedSync } from "@scure/bip39";

/**
* Ergo derivation path at change level
*/
export const ERGO_HD_CHANGE_PATH = "m/44'/429'/0'/0";

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

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

private constructor(hdKey: HDKey) {
/* istanbul ignore if -- @preserve */
if (!hdKey.publicKey) {
throw new Error("Public key is not present");
}

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

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

get privateKey(): Uint8Array | null {
return this._root.privateKey;
}

get chainCode(): Uint8Array | null {
return this._root.chainCode;
}

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

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

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

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

get address(): ErgoAddress {
return this._address;
}

static async fromMnemonic(mnemonic: string, options?: FromMnemonicOptions): Promise<ErgoHDKey> {
return this.fromMasterSeed(await mnemonicToSeed(mnemonic, options?.passphrase), options?.path);
}

static fromMnemonicSync(mnemonic: string, options?: FromMnemonicOptions): ErgoHDKey {
return this.fromMasterSeed(mnemonicToSeedSync(mnemonic, options?.passphrase), options?.path);
}

static fromMasterSeed(seed: Uint8Array, path = ERGO_HD_CHANGE_PATH): ErgoHDKey {
const key = HDKey.fromMasterSeed(seed);
if (path !== "") {
return new ErgoHDKey(key.derive(path));
}

return new ErgoHDKey(key);
}

static fromExtendedKey(base58EncodedExtKey: string): ErgoHDKey {
return new ErgoHDKey(HDKey.fromExtendedKey(base58EncodedExtKey));
}

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

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

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

return this;
}
}
2 changes: 2 additions & 0 deletions packages/wallet/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./ergoHDKey";
export * from "./mnemonic";
Loading

0 comments on commit 22aac26

Please sign in to comment.