-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathsetupDashClient.mjs
More file actions
554 lines (505 loc) · 16.6 KB
/
setupDashClient.mjs
File metadata and controls
554 lines (505 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
/* eslint-disable max-classes-per-file */
import {
EvoSDK,
IdentityPublicKeyInCreation,
IdentitySigner,
KeyType,
PlatformAddressSigner,
PrivateKey,
Purpose,
SecurityLevel,
wallet,
} from '@dashevo/evo-sdk';
// Load .env if dotenv is installed (optional — not needed for tutorials).
// Top-level await requires ESM — .mjs extension ensures this.
// eslint-disable-next-line import/no-extraneous-dependencies
try {
const { config } = await import('dotenv');
config();
} catch {
/* dotenv not installed */
}
// ⚠️ Tutorial helper — holds WIFs in memory for convenience.
// Do not use this pattern as-is for production key management.
// ###########################################################################
// # CONFIGURATION — edit these values for your environment #
// ###########################################################################
// Option 1: Edit the values below directly
// Option 2: Create a .env file with PLATFORM_MNEMONIC and NETWORK
const clientConfig = {
// The network to connect to ('testnet' or 'mainnet')
network: process.env.NETWORK || 'testnet',
// BIP39 mnemonic for wallet operations (identity & address tutorials).
// Leave as null for read-only tutorials.
mnemonic: process.env.PLATFORM_MNEMONIC || null,
// mnemonic: 'your twelve word mnemonic phrase goes here ...',
};
/**
* Build a DIP-13 identity key derivation path.
* Returns the full 7-level hardened path:
* m/9'/{coin}'/5'/0'/0'/{identityIndex}'/{keyIndex}'
*/
async function dip13KeyPath(network, identityIndex, keyIndex) {
const base =
network === 'testnet'
? await wallet.derivationPathDip13Testnet(5)
: await wallet.derivationPathDip13Mainnet(5);
return `${base.path}/0'/0'/${identityIndex}'/${keyIndex}'`;
}
// ---------------------------------------------------------------------------
// SDK client helpers
// ---------------------------------------------------------------------------
export async function createClient(network = 'testnet') {
const factories = {
testnet: () => EvoSDK.testnetTrusted(),
mainnet: () => EvoSDK.mainnetTrusted(),
local: () => EvoSDK.localTrusted(),
};
const factory = factories[network];
if (!factory) {
throw new Error(
`Unknown network "${network}". Use: ${Object.keys(factories).join(', ')}`,
);
}
const sdk = factory();
await sdk.connect();
return sdk;
}
// ---------------------------------------------------------------------------
// IdentityKeyManager
// ---------------------------------------------------------------------------
/** Key specs for the 5 standard identity keys (DIP-9). */
const KEY_SPECS = [
{
keyId: 0,
purpose: Purpose.AUTHENTICATION,
securityLevel: SecurityLevel.MASTER,
},
{
keyId: 1,
purpose: Purpose.AUTHENTICATION,
securityLevel: SecurityLevel.HIGH,
},
{
keyId: 2,
purpose: Purpose.AUTHENTICATION,
securityLevel: SecurityLevel.CRITICAL,
},
{
keyId: 3,
purpose: Purpose.TRANSFER,
securityLevel: SecurityLevel.CRITICAL,
},
{
keyId: 4,
purpose: Purpose.ENCRYPTION,
securityLevel: SecurityLevel.MEDIUM,
},
];
/**
* Manages identity keys and signing for write operations.
*
* Mirrors the old js-dash-sdk pattern where `setupDashClient()` hid all
* wallet/signing config. Construct once, then call getAuth(), getTransfer(),
* or getMaster() to get a ready-to-use { identity, identityKey, signer }.
*
* Keys are derived from a BIP39 mnemonic using standard DIP-9 paths
* (compatible with dash-evo-tool / Dash wallets):
* Key 0 = MASTER (identity updates)
* Key 1 = HIGH auth (documents, names)
* Key 2 = CRITICAL auth (contracts, documents, names)
* Key 3 = TRANSFER (credit transfers/withdrawals)
* Key 4 = ENCRYPTION MEDIUM (encrypted messaging/data)
*/
class IdentityKeyManager {
constructor(sdk, identityId, keys, identityIndex) {
this.sdk = sdk;
this.id = identityId;
this.keys = keys; // { master, auth, authHigh, transfer, encryption }
this.identityIndex = identityIndex ?? 0;
}
get identityId() {
return this.id;
}
/**
* Create an IdentityKeyManager from a BIP39 mnemonic.
* Derives all standard identity keys using DIP-9 paths.
*
* @param {object} opts
* @param {object} opts.sdk - Connected EvoSDK instance
* @param {string} [opts.identityId] - Identity ID. If omitted, auto-resolved
* from the mnemonic by looking up the master key's public key hash on-chain.
* @param {string} opts.mnemonic - BIP39 mnemonic
* @param {string} [opts.network='testnet'] - 'testnet' or 'mainnet'
* @param {number} [opts.identityIndex=0] - Which identity derived from this mnemonic
*/
static async create({
sdk,
identityId,
mnemonic,
network = 'testnet',
identityIndex = 0,
}) {
const derive = async (keyIndex) =>
wallet.deriveKeyFromSeedWithPath({
mnemonic,
path: await dip13KeyPath(network, identityIndex, keyIndex),
network,
});
const [masterKey, authHighKey, authKey, transferKey, encryptionKey] =
await Promise.all([
derive(0), // MASTER
derive(1), // HIGH auth
derive(2), // CRITICAL auth
derive(3), // TRANSFER
derive(4), // ENCRYPTION MEDIUM
]);
let resolvedId = identityId;
if (!resolvedId) {
const privateKey = PrivateKey.fromWIF(masterKey.toObject().privateKeyWif);
const pubKeyHash = privateKey.getPublicKeyHash();
const identity = await sdk.identities.byPublicKeyHash(pubKeyHash);
if (!identity) {
throw new Error(
'No identity found for the given mnemonic (key 0 public key hash)',
);
}
resolvedId = identity.id.toString();
}
return new IdentityKeyManager(
sdk,
resolvedId,
{
master: { keyId: 0, privateKeyWif: masterKey.toObject().privateKeyWif },
authHigh: {
keyId: 1,
privateKeyWif: authHighKey.toObject().privateKeyWif,
},
auth: { keyId: 2, privateKeyWif: authKey.toObject().privateKeyWif },
transfer: {
keyId: 3,
privateKeyWif: transferKey.toObject().privateKeyWif,
},
encryption: {
keyId: 4,
privateKeyWif: encryptionKey.toObject().privateKeyWif,
},
},
identityIndex,
);
}
/**
* Find the first unused DIP-9 identity index for a mnemonic.
* Scans indices starting at 0 until no on-chain identity is found.
*
* @param {object} sdk - Connected EvoSDK instance
* @param {string} mnemonic - BIP39 mnemonic
* @param {string} [network='testnet'] - 'testnet' or 'mainnet'
* @returns {Promise<number>} The first unused identity index
*/
static async findNextIndex(sdk, mnemonic, network = 'testnet') {
/* eslint-disable no-await-in-loop */
for (let i = 0; ; i += 1) {
const path = await dip13KeyPath(network, i, 0);
const key = await wallet.deriveKeyFromSeedWithPath({
mnemonic,
path,
network,
});
const privateKey = PrivateKey.fromWIF(key.toObject().privateKeyWif);
const existing = await sdk.identities.byPublicKeyHash(
privateKey.getPublicKeyHash(),
);
if (!existing) return i;
}
/* eslint-enable no-await-in-loop */
}
/**
* Create an IdentityKeyManager for a new (not yet registered) identity.
* Derives keys and stores public key data needed for identity creation.
* If identityIndex is omitted, auto-selects the next unused index.
*
* @param {object} opts
* @param {object} opts.sdk - Connected EvoSDK instance
* @param {string} opts.mnemonic - BIP39 mnemonic
* @param {string} [opts.network='testnet'] - 'testnet' or 'mainnet'
* @param {number} [opts.identityIndex] - Identity index (auto-scanned if omitted)
* @returns {Promise<IdentityKeyManager>}
*/
static async createForNewIdentity({
sdk,
mnemonic,
network = 'testnet',
identityIndex,
}) {
const idx =
identityIndex ??
(await IdentityKeyManager.findNextIndex(sdk, mnemonic, network));
const derive = async (keyIndex) =>
wallet.deriveKeyFromSeedWithPath({
mnemonic,
path: await dip13KeyPath(network, idx, keyIndex),
network,
});
const derivedKeys = await Promise.all(
KEY_SPECS.map((spec) => derive(spec.keyId)),
);
const keys = {
master: {
keyId: 0,
privateKeyWif: derivedKeys[0].toObject().privateKeyWif,
publicKey: derivedKeys[0].toObject().publicKey,
},
authHigh: {
keyId: 1,
privateKeyWif: derivedKeys[1].toObject().privateKeyWif,
publicKey: derivedKeys[1].toObject().publicKey,
},
auth: {
keyId: 2,
privateKeyWif: derivedKeys[2].toObject().privateKeyWif,
publicKey: derivedKeys[2].toObject().publicKey,
},
transfer: {
keyId: 3,
privateKeyWif: derivedKeys[3].toObject().privateKeyWif,
publicKey: derivedKeys[3].toObject().publicKey,
},
encryption: {
keyId: 4,
privateKeyWif: derivedKeys[4].toObject().privateKeyWif,
publicKey: derivedKeys[4].toObject().publicKey,
},
};
return new IdentityKeyManager(sdk, null, keys, idx);
}
/**
* Build IdentityPublicKeyInCreation objects for all 5 standard keys.
* Only works when public key data is available (via createForNewIdentity).
*
* @returns {IdentityPublicKeyInCreation[]}
*/
getKeysInCreation() {
return KEY_SPECS.map((spec) => {
const key = Object.values(this.keys).find((k) => k.keyId === spec.keyId);
if (!key?.publicKey) {
throw new Error(
`Public key data not available for key ${spec.keyId}. Use createForNewIdentity().`,
);
}
const pubKeyData = Uint8Array.from(Buffer.from(key.publicKey, 'hex'));
return new IdentityPublicKeyInCreation({
keyId: spec.keyId,
purpose: spec.purpose,
securityLevel: spec.securityLevel,
keyType: KeyType.ECDSA_SECP256K1,
data: pubKeyData,
});
});
}
/**
* Build an IdentitySigner loaded with all 5 key WIFs.
* Useful for identity creation where all keys must sign.
*
* @returns {IdentitySigner}
*/
getFullSigner() {
const signer = new IdentitySigner();
Object.values(this.keys).forEach((key) => {
signer.addKeyFromWif(key.privateKeyWif);
});
return signer;
}
/**
* Fetch identity and build { identity, identityKey, signer } for a given key.
* @param {string} keyName - One of: master, auth, authHigh, transfer, encryption
* @returns {{ identity, identityKey, signer }}
*/
async getSigner(keyName) {
if (!this.id) {
throw new Error(
'Identity ID is not set. Use IdentityKeyManager.create() for an existing identity, ' +
'or create/register the identity first and then set the ID.',
);
}
const key = this.keys[keyName];
if (!key) {
throw new Error(
`Unknown key "${keyName}". Use: ${Object.keys(this.keys).join(', ')}`,
);
}
const identity = await this.sdk.identities.fetch(this.id);
const identityKey = identity.getPublicKeyById(key.keyId);
const signer = new IdentitySigner();
signer.addKeyFromWif(key.privateKeyWif);
return { identity, identityKey, signer };
}
/** CRITICAL auth (key 2) — contracts, documents, names. */
async getAuth() {
return this.getSigner('auth');
}
/** HIGH auth (key 1) — documents, names. */
async getAuthHigh() {
return this.getSigner('authHigh');
}
/** TRANSFER — credit transfers, withdrawals. */
async getTransfer() {
return this.getSigner('transfer');
}
/** ENCRYPTION MEDIUM — encrypted messaging/data. */
async getEncryption() {
return this.getSigner('encryption');
}
/**
* MASTER — identity updates (add/disable keys).
* @param {string[]} [additionalKeyWifs] - WIFs for new keys being added
*/
async getMaster(additionalKeyWifs) {
const result = await this.getSigner('master');
if (additionalKeyWifs) {
additionalKeyWifs.forEach((wif) => result.signer.addKeyFromWif(wif));
}
return result;
}
}
// ---------------------------------------------------------------------------
// AddressKeyManager
// ---------------------------------------------------------------------------
/**
* Manages platform address keys and signing for address operations.
*
* Parallel to IdentityKeyManager but for platform address operations.
* Derives BIP44 keys from a mnemonic and provides ready-to-use
* PlatformAddressSigner instances.
*
* Platform addresses are bech32m-encoded L2 addresses (tdash1... on testnet)
* that hold credits directly, independent of identities.
*/
class AddressKeyManager {
constructor(sdk, addresses, network) {
this.sdk = sdk;
this.addresses = addresses; // [{ address, bech32m, privateKeyWif, path }]
this.network = network;
}
/** The first derived address (index 0). */
get primaryAddress() {
return this.addresses[0];
}
/**
* Create an AddressKeyManager from a BIP39 mnemonic.
* Derives platform address keys using BIP44 paths.
*
* @param {object} opts
* @param {object} opts.sdk - Connected EvoSDK instance
* @param {string} opts.mnemonic - BIP39 mnemonic
* @param {string} [opts.network='testnet'] - 'testnet' or 'mainnet'
* @param {number} [opts.count=1] - Number of addresses to derive
*/
static async create({ sdk, mnemonic, network = 'testnet', count = 1 }) {
const addresses = [];
/* eslint-disable no-await-in-loop */
for (let i = 0; i < count; i += 1) {
const pathInfo =
network === 'testnet'
? await wallet.derivationPathBip44Testnet(0, 0, i)
: await wallet.derivationPathBip44Mainnet(0, 0, i);
const { path } = pathInfo;
const keyInfo = await wallet.deriveKeyFromSeedWithPath({
mnemonic,
path,
network,
});
const obj = keyInfo.toObject();
const privateKey = PrivateKey.fromWIF(obj.privateKeyWif);
const signer = new PlatformAddressSigner();
const platformAddress = signer.addKey(privateKey);
addresses.push({
address: platformAddress,
bech32m: platformAddress.toBech32m(network),
privateKeyWif: obj.privateKeyWif,
path,
});
}
/* eslint-enable no-await-in-loop */
return new AddressKeyManager(sdk, addresses, network);
}
/**
* Create a PlatformAddressSigner with the primary key loaded.
* @returns {PlatformAddressSigner}
*/
getSigner() {
const signer = new PlatformAddressSigner();
const privateKey = PrivateKey.fromWIF(this.primaryAddress.privateKeyWif);
signer.addKey(privateKey);
return signer;
}
/**
* Create a PlatformAddressSigner with all derived keys loaded.
* @returns {PlatformAddressSigner}
*/
getFullSigner() {
const signer = new PlatformAddressSigner();
this.addresses.forEach((addr) => {
const privateKey = PrivateKey.fromWIF(addr.privateKeyWif);
signer.addKey(privateKey);
});
return signer;
}
/**
* Fetch current balance and nonce for the primary address.
* @returns {Promise<PlatformAddressInfo|undefined>}
*/
async getInfo() {
return this.sdk.addresses.get(this.primaryAddress.bech32m);
}
/**
* Fetch current balance and nonce for an address by index.
* @param {number} index - Address index
* @returns {Promise<PlatformAddressInfo|undefined>}
*/
async getInfoAt(index) {
const entry = this.addresses[index];
if (!entry) {
throw new Error(
`No derived address at index ${index} (count=${this.addresses.length})`,
);
}
return this.sdk.addresses.get(entry.bech32m);
}
}
// ---------------------------------------------------------------------------
// setupDashClient — convenience wrapper
// ---------------------------------------------------------------------------
export async function setupDashClient({
requireIdentity = true,
identityIndex,
} = {}) {
const { network, mnemonic } = clientConfig;
const sdk = await createClient(network);
let keyManager;
let addressKeyManager;
if (mnemonic) {
addressKeyManager = await AddressKeyManager.create({
sdk,
mnemonic,
network,
});
if (requireIdentity) {
keyManager = await IdentityKeyManager.create({
sdk,
mnemonic,
network,
identityIndex,
});
} else {
keyManager = await IdentityKeyManager.createForNewIdentity({
sdk,
mnemonic,
network,
identityIndex,
});
}
}
return { sdk, keyManager, addressKeyManager };
}
export { IdentityKeyManager, AddressKeyManager, clientConfig };