Skip to content

Commit 778bfce

Browse files
authored
feat(wallet-lib): rework storage for multiple key chains (#231)
1 parent e74948b commit 778bfce

25 files changed

Lines changed: 754 additions & 34 deletions
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {PrivateKey, Network,} from "../types";
2+
import {HDPrivateKey, HDPublicKey} from "@dashevo/dashcore-lib";
3+
import {Transaction} from "@dashevo/dashcore-lib/typings/transaction/Transaction";
4+
5+
export declare namespace DerivableKeyChain {
6+
interface IDerivableKeyChainOptions {
7+
network?: Network;
8+
keys?: [Keys]
9+
}
10+
}
11+
12+
export declare class DerivableKeyChain {
13+
constructor(options?: DerivableKeyChain.IDerivableKeyChainOptions);
14+
network: Network;
15+
keys: [Keys];
16+
17+
type: HDKeyTypesParam|PrivateKeyTypeParam;
18+
HDPrivateKey?: HDPrivateKey;
19+
privateKey?: PrivateKey;
20+
21+
generateKeyForChild(index: number, type?: HDKeyTypesParam): HDPrivateKey|HDPublicKey;
22+
generateKeyForPath(path: string, type?: HDKeyTypesParam): HDPrivateKey|HDPublicKey;
23+
24+
getDIP15ExtendedKey(userUniqueId: string, contactUniqueId: string, index?: number, accountIndex?: number, type?: HDKeyTypesParam): HDKeyTypes;
25+
getHardenedDIP15AccountKey(index?: number, type?: HDKeyTypesParam): HDKeyTypes;
26+
getHardenedBIP44HDKey(type?: HDKeyTypesParam): HDKeyTypes;
27+
getHardenedDIP9FeatureHDKey(type?: HDKeyTypesParam): HDKeyTypes;
28+
getKeyForChild(index: number, type?: HDKeyTypesParam): HDKeyTypes;
29+
getKeyForPath(path: string, type?: HDKeyTypesParam): HDKeyTypes;
30+
getPrivateKey(): PrivateKey;
31+
32+
sign(object: Transaction|any, privateKeys:[PrivateKey], sigType: number): any;
33+
}
34+
35+
type HDKeyTypes = HDPublicKey | HDPrivateKey;
36+
37+
export declare enum HDKeyTypesParam {
38+
HDPrivateKey="HDPrivateKey",
39+
HDPublicKey="HDPrivateKey",
40+
}
41+
export declare enum PrivateKeyTypeParam {
42+
privateKey='privateKey'
43+
}
44+
export declare interface Keys {
45+
[path: string]: {
46+
path: string
47+
};
48+
}
49+
50+
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const { Networks, HDPrivateKey, HDPublicKey } = require('@dashevo/dashcore-lib');
2+
const { PrivateKey, PublicKey } = require('@dashevo/dashcore-lib');
3+
const { doubleSha256 } = require('../../utils/crypto');
4+
const { mnemonicToHDPrivateKey } = require('../../utils/mnemonic');
5+
6+
function generateKeyChainId(key) {
7+
const keyChainIdSuffix = doubleSha256(key.toString()).toString('hex').slice(0, 10);
8+
return `kc${keyChainIdSuffix}`;
9+
}
10+
11+
function fromOptions(opts) {
12+
let rootKey;
13+
let rootKeyType;
14+
let network = Networks.testnet.toString();
15+
let passphrase = '';
16+
17+
if (opts) {
18+
if (opts.passphrase) {
19+
passphrase = opts.passphrase;
20+
}
21+
if (opts.mnemonic) {
22+
rootKeyType = 'HDPrivateKey';
23+
rootKey = (typeof opts.mnemonic === 'string') ? HDPrivateKey(opts.HDPrivateKey) : opts.HDPrivateKey;
24+
}
25+
if (opts.network) {
26+
network = opts.network;
27+
}
28+
if (opts.HDPrivateKey) {
29+
rootKeyType = 'HDPrivateKey';
30+
rootKey = (typeof opts.HDPrivateKey === 'string') ? HDPrivateKey(opts.HDPrivateKey) : opts.HDPrivateKey;
31+
network = rootKey.network.toString();
32+
} else if (opts.HDPublicKey) {
33+
rootKeyType = 'HDPublicKey';
34+
rootKey = (typeof opts.HDPublicKey === 'string') ? HDPublicKey(opts.HDPublicKey) : opts.HDPublicKey;
35+
network = rootKey.network.toString();
36+
} else if (opts.privateKey) {
37+
rootKeyType = 'privateKey';
38+
rootKey = (typeof opts.privateKey === 'string') ? new PrivateKey(opts.privateKey, opts.network) : opts.privateKey;
39+
network = rootKey.network.toString();
40+
} else if (opts.publicKey) {
41+
rootKeyType = 'publicKey';
42+
rootKey = (typeof opts.publicKey === 'string') ? new PublicKey(opts.publicKey, opts.network) : opts.publicKey;
43+
network = rootKey.network.toString();
44+
} else if (opts.address) {
45+
rootKeyType = 'address';
46+
rootKey = opts.address.toString();
47+
} else if (opts.mnemonic) {
48+
return fromOptions({
49+
...opts,
50+
HDPrivateKey: mnemonicToHDPrivateKey(opts.mnemonic, network, passphrase),
51+
});
52+
}
53+
}
54+
55+
const lookAheadOpts = {
56+
isWatched: true,
57+
paths: {},
58+
...opts.lookAheadOpts,
59+
};
60+
61+
return {
62+
rootKeyType,
63+
rootKey,
64+
network,
65+
passphrase,
66+
lookAheadOpts,
67+
};
68+
}
69+
70+
class DerivableKeyChain {
71+
constructor(opts = {}) {
72+
const {
73+
rootKey,
74+
rootKeyType,
75+
network,
76+
lookAheadOpts,
77+
} = fromOptions(opts);
78+
if (!rootKeyType || !rootKey) {
79+
throw new Error('Expect one of [mnemonic, HDPrivateKey, HDPublicKey, privateKey, publicKey, address] to be provided.');
80+
}
81+
this.keyChainId = generateKeyChainId(rootKey);
82+
83+
this.rootKey = rootKey;
84+
this.network = network;
85+
this.rootKeyType = rootKeyType;
86+
this.lookAheadOpts = { isWatched: true, ...lookAheadOpts };
87+
88+
this.issuedPaths = new Map();
89+
90+
this.maybeLookAhead();
91+
}
92+
}
93+
DerivableKeyChain.prototype.getForPath = require('./methods/getForPath');
94+
DerivableKeyChain.prototype.getForAddress = require('./methods/getForAddress');
95+
DerivableKeyChain.prototype.getDIP15ExtendedKey = require('./methods/getDIP15ExtendedKey');
96+
DerivableKeyChain.prototype.getFirstUnusedAddress = require('./methods/getFirstUnusedAddress');
97+
DerivableKeyChain.prototype.getHardenedBIP44HDKey = require('./methods/getHardenedBIP44HDKey');
98+
DerivableKeyChain.prototype.getHardenedDIP9FeatureHDKey = require('./methods/getHardenedDIP9FeatureHDKey');
99+
DerivableKeyChain.prototype.getHardenedDIP15AccountKey = require('./methods/getHardenedDIP15AccountKey');
100+
DerivableKeyChain.prototype.getRootKey = require('./methods/getRootKey');
101+
DerivableKeyChain.prototype.getWatchedAddresses = require('./methods/getWatchedAddresses');
102+
DerivableKeyChain.prototype.getIssuedPaths = require('./methods/getIssuedPaths');
103+
DerivableKeyChain.prototype.maybeLookAhead = require('./methods/maybeLookAhead');
104+
DerivableKeyChain.prototype.markAddressAsUsed = require('./methods/markAddressAsUsed');
105+
DerivableKeyChain.prototype.sign = require('./methods/sign');
106+
107+
module.exports = DerivableKeyChain;
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
const Dashcore = require('@dashevo/dashcore-lib');
2+
const { expect } = require('chai');
3+
const DerivableKeyChain = require('./DerivableKeyChain');
4+
const { mnemonicToHDPrivateKey } = require('../../utils/mnemonic');
5+
6+
let derivableKeyChain;
7+
let derivableKeyChain2;
8+
const mnemonic = 'during develop before curtain hazard rare job language become verb message travel';
9+
const mnemonic2 = 'birth kingdom trash renew flavor utility donkey gasp regular alert pave layer';
10+
const hdPublicKey = 'xpub661MyMwAqRbcFGB6XSWBsD725rJDUbFUpy4zWe2u22nJ2BxpoHFxtVDfKnTnvVQHohnY7AsVpRTHDv6PyPQTYu1KxFPKw29MAVXPEpz1G7V';
11+
const expectedRootDIP15AccountKey_0 = 'tprv8hRzmheQujhJN5XP2dj955nAFCKeEoSifJRWuutdbwWRtusdDQ426jbp75EqErUSuTxmPyxYmP1TpcF5qdxGhXLNXRLMGsRLG6NFCv1WnaQ';
12+
const expectedRootDIP15AccountKey_1 = 'tprv8hRzmheQujhJQyCtFTuUFHxB3Ag5VLB994zhH4CfxbA41cq73HT2mpYq5M33V54oJyn6g514saxxVJB886G55eYX56J6D6x87UNNT6iQHkR';
13+
const expectedKeyForChild_0 = 'tprv8d4podc2Tg459CH2bwLHXj3vdJFBT2rdsk5Nr1djH7hzHdt5LRdvN6QyFwMiDy7ffRdik7fEVRKKgsHB4F18sh8xF6jFXpKq4sUgGBoSbKw';
14+
15+
describe('DerivableKeyChain', function suite() {
16+
this.timeout(1000);
17+
it('should create a DerivableKeyChain', () => {
18+
const expectedException1 = 'Expect one of [mnemonic, HDPrivateKey, HDPublicKey, privateKey, publicKey, address] to be provided.';
19+
expect(() => new DerivableKeyChain()).to.throw(expectedException1);
20+
21+
derivableKeyChain = new DerivableKeyChain({ mnemonic: mnemonic, network: 'testnet' });
22+
expect(derivableKeyChain.rootKeyType).to.equal('HDPrivateKey');
23+
expect(derivableKeyChain.network.toString()).to.equal('testnet');
24+
expect(derivableKeyChain.rootKey.network.toString()).to.equal('testnet');
25+
26+
derivableKeyChain2 = new DerivableKeyChain({ mnemonic: mnemonic2, network: 'livenet' });
27+
});
28+
29+
it('should generate key for full path', () => {
30+
const path = 'm/44\'/1\'/0\'/0/0';
31+
const pk2 = derivableKeyChain.getForPath(path).key;
32+
const address = new Dashcore.Address(pk2.publicKey.toAddress()).toString();
33+
expect(address).to.equal('yNfUebksUc5HoSfg8gv98ruC3jUNJUM8pT');
34+
});
35+
36+
it('should get DIP15 account key', function () {
37+
const rootDIP15AccountKey_0 = derivableKeyChain.getHardenedDIP15AccountKey(0);
38+
expect(rootDIP15AccountKey_0.toString()).to.deep.equal(expectedRootDIP15AccountKey_0);
39+
const rootDIP15AccountKey_1 = derivableKeyChain.getHardenedDIP15AccountKey(1);
40+
expect(rootDIP15AccountKey_1.toString()).to.deep.equal(expectedRootDIP15AccountKey_1);
41+
});
42+
43+
it('should get DIP15 extended key', function () {
44+
const userUniqueId = '0x555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a';
45+
const contactUniqueId = '0xa137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5';
46+
47+
// m/9'/5'/15'/0'/0x555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a'/0xa137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5'/0
48+
const DIP15ExtPubKey_0 = derivableKeyChain2.getDIP15ExtendedKey(userUniqueId, contactUniqueId, 0, 0, 'HDPublicKey');
49+
expect(DIP15ExtPubKey_0.toString()).to.equal('xpub6LTkTQFSb8KMgMSz4B6sMZLpkQAY6wSTDprDkHDmLwWLpnjxazuxZn13FrSLKUafitsxuaaffM5a49P6aswhpppWUuYW6eFnwBXshR2W2eY');
50+
expect(DIP15ExtPubKey_0.publicKey.toString()).to.equal('038030c88ab0106e1f4af3b939db2bafc56f892554106f08da1ce1f9ef10f807bd')
51+
52+
const DIP15ExtPrivKey_0 = derivableKeyChain2.getDIP15ExtendedKey(userUniqueId, contactUniqueId, 0, 0);
53+
expect(DIP15ExtPrivKey_0.toString()).to.equal('xprvA7UQ3tiYkkm4TsNWx9ZrzRQ6CNL3hUibrbvcwtp9nbyMwzQp3Tbi1ygZQaPoigDhCf8XUjMmGK2NbnB2kLXPYg99Lp6e3iki318sdWcFN3q');
54+
expect(DIP15ExtPrivKey_0.privateKey.toString()).to.equal('fac40790776d171ee1db90899b5eb2df2f7d2aaf35ad56f07ffb8ed2c57f8e60')
55+
expect(DIP15ExtPrivKey_0.publicKey.toString()).to.equal('038030c88ab0106e1f4af3b939db2bafc56f892554106f08da1ce1f9ef10f807bd')
56+
57+
// This comes from the test factor of DIP-15
58+
const userAhash = "0xa11ce14f698b32e9bb306dba7bbbee831263dcf658abeebb39930460ead117e5";
59+
const userBhash = "0xb0b052ff075c5ca3c16c3e20e9ac8223834475cc1324ab07889cb24ce6a62793";
60+
const DIP15ExtKey_1 = derivableKeyChain.getDIP15ExtendedKey(userAhash, userBhash, 0, 0);
61+
expect(DIP15ExtKey_1.privateKey.toString()).to.equal('60581b6dca8244d3fb3cfe619b5a22277e5423b01e5285f356981f247e0f4a60')
62+
expect(DIP15ExtKey_1.publicKey.toString()).to.equal('03deaac00f721151307fbc7bf80d7b8afab98c1f026d67e5f56b21e2013f551ce6')
63+
});
64+
65+
it('should derive from hardened path and get address', () => {
66+
const hardenedHDKey = derivableKeyChain.getHardenedBIP44HDKey();
67+
const pk2 = derivableKeyChain.getForPath(`m/44'/1'`).key;
68+
expect(pk2.toString()).to.equal(hardenedHDKey.toString());
69+
expect(hardenedHDKey.toString()).to.deep.equal('tprv8dtrJNytYHRiZY585hmHGbguS6VjGpK49puSB7oXZjLHcQfrAzQkF4ZCxM2DkEbyY85J4EYcZ8EjT5ZCU8ozB727TDdodbfXet5GkGau2RQ');
70+
const derivedPk = hardenedHDKey.deriveChild(0, true).deriveChild(0).deriveChild(0);
71+
// m/44'/1'/0'/0/0 (this is first external address of the account 0)
72+
const address = new Dashcore.Address(derivedPk.publicKey.toAddress()).toString();
73+
expect(address).to.equal('yNfUebksUc5HoSfg8gv98ruC3jUNJUM8pT');
74+
});
75+
76+
it('should get hardened DIP9FeatureHDKey', function () {
77+
const hardenedHDKey = derivableKeyChain.getHardenedDIP9FeatureHDKey();
78+
const pk2 = derivableKeyChain.getForPath(`m/9'/1'`).key;
79+
expect(pk2.toString()).to.equal(hardenedHDKey.toString());
80+
expect(hardenedHDKey.toString()).to.deep.equal('tprv8fBJjWoGgCpGRCbyzE9RUA59rmoN1RUijhLnXGL4VHnLxvSe523yVg4GrGzbR6TyXtdynAEh5z8UX55EXt2Cb3xjvrsx2PgTY9BHxzFVkWn');
81+
});
82+
83+
it('should get key for path using the HDPrivateKey', () => {
84+
const derivableKeyChain2 = new DerivableKeyChain({ HDPrivateKey: mnemonicToHDPrivateKey(mnemonic, 'testnet') });
85+
const keyForChild = derivableKeyChain2.getForPath('m/0').key;
86+
expect(keyForChild.toString()).to.equal(expectedKeyForChild_0);
87+
});
88+
89+
it('should mark address watched and get watched addresses', function () {
90+
const key0 = derivableKeyChain.getForPath('m/0');
91+
derivableKeyChain.getForPath('m/0').isWatched = true
92+
key0.isWatched = true;
93+
derivableKeyChain.getForPath('m/1', { isWatched: false });
94+
derivableKeyChain.getForPath('m/2', { isWatched: true });
95+
96+
const watchedAddresses = derivableKeyChain.getWatchedAddresses();
97+
let expectedWatchedAddresses = [
98+
derivableKeyChain.getForPath('m/0').address.toString(),
99+
derivableKeyChain.getForPath('m/2').address.toString()
100+
];
101+
expect(watchedAddresses).to.deep.equal(expectedWatchedAddresses);
102+
});
103+
104+
it('should get watched addresses', function () {
105+
derivableKeyChain.getForPath('m/1').isWatched = true
106+
const watchedAddresses = derivableKeyChain.getWatchedAddresses();
107+
const expectedWatchedAddresses = [
108+
'ybQDfNwiDjk8ZH5UUmHQzAMEmjbrbK5dAj',
109+
'yhFX5rseJPitV45HUCaa9haeGHtLuooBaq',
110+
'yhqxsmYk6jfoGWf1hJKq7d4U2cGHCgzpFU'
111+
]
112+
expect(watchedAddresses).to.deep.equal(expectedWatchedAddresses);
113+
});
114+
115+
it('should remove an address from watched addresses', function () {
116+
derivableKeyChain.getForPath('m/0', { isWatched: false });
117+
derivableKeyChain.getForPath('m/1');
118+
const data2 = derivableKeyChain.getForPath('m/2');
119+
data2.isWatched = false;
120+
121+
expect(derivableKeyChain.getWatchedAddresses().length).to.equal(1);
122+
});
123+
124+
it('should get address for path', function (){
125+
const address0_1 = derivableKeyChain.getForPath('m/1').address;
126+
expect(address0_1.toString()).to.equal('yhFX5rseJPitV45HUCaa9haeGHtLuooBaq')
127+
})
128+
129+
it('should mark address as used', function () {
130+
const address0_0 = derivableKeyChain.getForPath('m/0').address;
131+
derivableKeyChain.markAddressAsUsed(address0_0);
132+
expect(derivableKeyChain.issuedPaths.get('m/0').isUsed).to.equal(true)
133+
});
134+
});
135+
136+
describe('DerivableKeyChain - HDPublicKey', function suite(){
137+
let hdpubDerivableKeyChain;
138+
it('should initiate from a HDPublicKey', function () {
139+
hdpubDerivableKeyChain = new DerivableKeyChain({
140+
HDPublicKey: new Dashcore.HDPublicKey(hdPublicKey),
141+
network: 'testnet'
142+
});
143+
// As the HDPublicKey starts with xpub, it's livenet and should take priority over our network being set.
144+
expect(hdpubDerivableKeyChain.network.toString()).to.equal('livenet');
145+
expect(hdpubDerivableKeyChain.keyChainId).to.equal('kc5059442d66');
146+
expect(hdpubDerivableKeyChain.getRootKey().toString()).to.equal(hdPublicKey);
147+
});
148+
149+
it('should derivate', function () {
150+
const key0_1 = hdpubDerivableKeyChain.getForPath('m/1').key;
151+
expect(key0_1.publicKey.toAddress(hdpubDerivableKeyChain.network).toString()).to.equal('XoL5LcBiDWcj6L7fFwytsFoX5Vz7BVXw9w')
152+
});
153+
154+
it('should get address for path', function (){
155+
const address0_1 = hdpubDerivableKeyChain.getForPath('m/2').address;
156+
expect(address0_1.toString()).to.equal('XwAzpxQKbgebaLiadq1c6rDeFJ4FKPUufy')
157+
})
158+
})
159+
160+
describe('DerivableKeyChain - single privateKey', function suite() {
161+
this.timeout(10000);
162+
163+
it('should correctly throw errors out when not a HDPublicKey (privateKey)', () => {
164+
const privateKey = Dashcore.PrivateKey().toString();
165+
const network = 'livenet';
166+
const pkDerivableKeyChain = new DerivableKeyChain({ privateKey, network });
167+
expect(pkDerivableKeyChain.network).to.equal(network);
168+
expect(pkDerivableKeyChain.rootKeyType).to.equal('privateKey');
169+
expect(pkDerivableKeyChain.rootKey.toString()).to.equal(privateKey);
170+
171+
const expectedException1 = 'Wallet is not loaded from a mnemonic or a HDPrivateKey, impossible to derivate keys for path m/0';
172+
expect(() => pkDerivableKeyChain.getForPath('m/0')).to.throw(expectedException1);
173+
});
174+
175+
it('should get private key', () => {
176+
const privateKey = Dashcore.PrivateKey().toString();
177+
const pkDerivableKeyChain = new DerivableKeyChain({ privateKey, network: 'livenet' });
178+
expect(pkDerivableKeyChain.getRootKey().toString()).to.equal(privateKey);
179+
expect(pkDerivableKeyChain.rootKey.toString()).to.equal(privateKey);
180+
});
181+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Return the extended key of the relationship between two dashpay contacts.
3+
* @param userUniqueId - Current userID
4+
* @param contactUniqueId - Contact userID
5+
* @param index - the key index.
6+
* @param accountIndex[=0] - the internal wallet account from which derivation is done
7+
* @param type {HDPrivateKey|HDPublicKey} [type=HDPrivateKey] - set the type of returned keys
8+
* @return {HDPrivateKey|HDPublicKey}
9+
*/
10+
function getDIP15ExtendedKey(userUniqueId, contactUniqueId, index = 0, accountIndex = 0, type = 'HDPrivateKey') {
11+
if (!['HDPrivateKey', 'HDPublicKey'].includes(this.rootKeyType)) {
12+
throw new Error('Wallet is not loaded from a mnemonic or a HDPubKey, impossible to derivate keys');
13+
}
14+
if (!userUniqueId || !contactUniqueId) throw new Error('Required userUniqueId and contactUniqueId to be defined');
15+
16+
// Require a HDPrivateKey for hardened derivation
17+
const extendedPrivateKey = this
18+
.getHardenedDIP15AccountKey(accountIndex, 'HDPrivateKey')
19+
.deriveChild((userUniqueId), true)
20+
.deriveChild((contactUniqueId), true)
21+
.deriveChild(index, false);
22+
23+
return (type === 'HDPublicKey' ? extendedPrivateKey.hdPublicKey : extendedPrivateKey);
24+
}
25+
26+
module.exports = getDIP15ExtendedKey;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
function getFirstUnusedAddress() {
2+
const allUnused = this.getIssuedPaths()
3+
.filter((path) => path.isUsed === false);
4+
5+
const firstUnused = allUnused.slice(0, 1)[0];
6+
7+
return {
8+
path: firstUnused.path,
9+
address: firstUnused.address.toString(),
10+
};
11+
}
12+
module.exports = getFirstUnusedAddress;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
function getForAddress(address) {
2+
const searchResult = [...this.issuedPaths.entries()]
3+
.find(([, el]) => el.address.toString() === address.toString());
4+
5+
if (!searchResult) {
6+
return null;
7+
}
8+
const [path] = searchResult;
9+
return this.getForPath(path);
10+
}
11+
12+
module.exports = getForAddress;

0 commit comments

Comments
 (0)