-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(wallet): add
ErgoHDKey
for key handling
- Loading branch information
1 parent
43540c1
commit 22aac26
Showing
16 changed files
with
495 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./ergoHDKey"; | ||
export * from "./mnemonic"; |
Oops, something went wrong.