Skip to content

Commit 54199e3

Browse files
committed
Add ethereum module for wallet generation.
1 parent 212ac42 commit 54199e3

File tree

6 files changed

+283
-0
lines changed

6 files changed

+283
-0
lines changed

lib/ethereum/config.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*!
2+
* Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
3+
*/
4+
// TODO: Later figure out the right place for this config.
5+
6+
/**
7+
* Default derivation path for Ethereum-compatible wallets.
8+
* BIP-49 path: m/49'/60'/0'/0/0
9+
* - 49' = BIP-49 (used for USBC/Omnumi compatibility).
10+
* - 60' = Ethereum coin type.
11+
* - 0' = account.
12+
* - 0 = change (external).
13+
* - 0 = address index.
14+
*/
15+
export const DEFAULT_DERIVATION_PATH = 'm/49\'/60\'/0\'/0/0';
16+
17+
/**
18+
* Default mnemonic strength (128 bits = 12 words).
19+
*/
20+
export const DEFAULT_MNEMONIC_STRENGTH = 128;

lib/ethereum/index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*!
2+
* Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
3+
*/
4+
5+
// Configuration exports
6+
export {
7+
DEFAULT_DERIVATION_PATH,
8+
DEFAULT_MNEMONIC_STRENGTH
9+
} from './config.js';
10+
11+
// Wallet generation exports
12+
export {
13+
generateMnemonic,
14+
walletFromMnemonic
15+
} from './wallet.js';
16+
17+
// Utility exports
18+
export {
19+
isValidPrivateKey,
20+
isValidDerivationPath
21+
} from './utils.js';

lib/ethereum/utils.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*!
2+
* Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
3+
*/
4+
5+
/**
6+
* Validates a private key.
7+
*
8+
* @param {string} privateKey - The private key to validate.
9+
*
10+
* @returns {boolean} True if valid, false otherwise.
11+
*/
12+
export function isValidPrivateKey(privateKey) {
13+
if(typeof privateKey !== 'string') {
14+
return false;
15+
}
16+
// Check format: 0x followed by 64 hex characters (32 bytes)
17+
return /^0x[a-fA-F0-9]{64}$/.test(privateKey);
18+
}
19+
20+
/**
21+
* Validates a derivation path.
22+
*
23+
* @param {string} path - The derivation path to validate.
24+
*
25+
* @returns {boolean} True if valid, false otherwise.
26+
*/
27+
export function isValidDerivationPath(path) {
28+
if(typeof path !== 'string') {
29+
return false;
30+
}
31+
// Generic BIP derivation path validation (works for BIP-44, BIP-49, etc.)
32+
// Format: m/purpose'/coin'/account'/change/index
33+
// Example: m/49'/60'/0'/0/0 (USBC default)
34+
return /^m(\/\d+'?)+$/.test(path);
35+
}

lib/ethereum/wallet.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*!
2+
* Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
3+
*/
4+
import {DEFAULT_DERIVATION_PATH, DEFAULT_MNEMONIC_STRENGTH} from './config.js';
5+
import {ethers} from 'ethers';
6+
7+
/**
8+
* Generates a new BIP39 mnemonic phrase.
9+
*
10+
* @param {object} [options] - Options.
11+
* @param {number} [options.strength=128] - Entropy strength in bits.
12+
* 128 = 12 words, 256 = 24 words.
13+
*
14+
* @returns {string} The generated mnemonic phrase.
15+
*/
16+
export function generateMnemonic({strength = DEFAULT_MNEMONIC_STRENGTH} = {}) {
17+
const entropy = ethers.randomBytes(strength / 8);
18+
return ethers.Mnemonic.entropyToPhrase(entropy);
19+
}
20+
21+
/**
22+
* Derives an Ethereum wallet from a mnemonic phrase.
23+
*
24+
* @param {object} options - Options.
25+
* @param {string} options.mnemonic - The BIP39 mnemonic phrase.
26+
* @param {string} [options.path] - Derivation path (default: m/49'/60'/0'/0/0).
27+
*
28+
* @returns {object} Wallet object with address, privateKey, publicKey, path.
29+
*/
30+
export function walletFromMnemonic({
31+
mnemonic,
32+
path = DEFAULT_DERIVATION_PATH
33+
} = {}) {
34+
// ethers.js validates mnemonic internally, throws if invalid
35+
const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic, undefined, path);
36+
37+
return {
38+
address: hdNode.address,
39+
privateKey: hdNode.privateKey,
40+
publicKey: hdNode.publicKey,
41+
derivationPath: path,
42+
mnemonic
43+
};
44+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"base64url-universal": "^2.0.0",
3636
"did-veres-one": "^16.0.1",
3737
"ed25519-signature-2018-context": "^1.1.0",
38+
"ethers": "^6.13.0",
3839
"json-pointer": "^0.6.2",
3940
"jsonld-signatures": "^11.3.0",
4041
"p-all": "^5.0.0",

test/web/15-wallet-test.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*!
2+
* Copyright (c) 2024 Digital Bazaar, Inc. All rights reserved.
3+
*/
4+
import {
5+
DEFAULT_DERIVATION_PATH,
6+
generateMnemonic,
7+
walletFromMnemonic,
8+
} from '../../lib/ethereum/index.js';
9+
10+
describe('Ethereum Module', function() {
11+
describe('generateMnemonic()', function() {
12+
it('should generate a 12-word mnemonic by default', function() {
13+
const mnemonic = generateMnemonic();
14+
15+
should.exist(mnemonic);
16+
mnemonic.should.be.a('string');
17+
18+
const words = mnemonic.trim().split(/\s+/);
19+
words.length.should.equal(12);
20+
});
21+
22+
it('should generate a 24-word mnemonic with strength=256', function() {
23+
const mnemonic = generateMnemonic({strength: 256});
24+
25+
should.exist(mnemonic);
26+
const words = mnemonic.trim().split(/\s+/);
27+
words.length.should.equal(24);
28+
});
29+
30+
it('should generate unique mnemonics each time', function() {
31+
const mnemonic1 = generateMnemonic();
32+
const mnemonic2 = generateMnemonic();
33+
34+
mnemonic1.should.not.equal(mnemonic2);
35+
});
36+
});
37+
38+
describe('walletFromMnemonic()', function() {
39+
const testMnemonic =
40+
'abandon abandon abandon abandon abandon abandon ' +
41+
'abandon abandon abandon abandon abandon about';
42+
43+
it('should derive a wallet from a valid mnemonic', function() {
44+
const wallet = walletFromMnemonic({mnemonic: testMnemonic});
45+
46+
should.exist(wallet);
47+
should.exist(wallet.address);
48+
should.exist(wallet.privateKey);
49+
should.exist(wallet.publicKey);
50+
should.exist(wallet.derivationPath);
51+
should.exist(wallet.mnemonic);
52+
});
53+
54+
it('should return a valid Ethereum address (0x + 40 hex chars)',
55+
function() {
56+
const wallet = walletFromMnemonic({mnemonic: testMnemonic});
57+
wallet.address.should.match(/^0x[a-fA-F0-9]{40}$/);
58+
});
59+
60+
it('should return a valid private key (0x + 64 hex chars)', function() {
61+
const wallet = walletFromMnemonic({mnemonic: testMnemonic});
62+
wallet.privateKey.should.match(/^0x[a-fA-F0-9]{64}$/);
63+
});
64+
65+
it('should use default derivation path m/49\'/60\'/0\'/0/0', function() {
66+
const wallet = walletFromMnemonic({mnemonic: testMnemonic});
67+
68+
wallet.derivationPath.should.equal(DEFAULT_DERIVATION_PATH);
69+
wallet.derivationPath.should.equal('m/49\'/60\'/0\'/0/0');
70+
});
71+
72+
it('should derive the same wallet from the same mnemonic', function() {
73+
const wallet1 = walletFromMnemonic({mnemonic: testMnemonic});
74+
const wallet2 = walletFromMnemonic({mnemonic: testMnemonic});
75+
76+
wallet1.address.should.equal(wallet2.address);
77+
wallet1.privateKey.should.equal(wallet2.privateKey);
78+
wallet1.publicKey.should.equal(wallet2.publicKey);
79+
});
80+
81+
it('should derive different wallets from different mnemonics', function() {
82+
const mnemonic1 = generateMnemonic();
83+
const mnemonic2 = generateMnemonic();
84+
85+
const wallet1 = walletFromMnemonic({mnemonic: mnemonic1});
86+
const wallet2 = walletFromMnemonic({mnemonic: mnemonic2});
87+
88+
wallet1.address.should.not.equal(wallet2.address);
89+
wallet1.privateKey.should.not.equal(wallet2.privateKey);
90+
});
91+
92+
it('should support custom derivation path', function() {
93+
const customPath = 'm/44\'/60\'/0\'/0/0';
94+
const wallet = walletFromMnemonic({
95+
mnemonic: testMnemonic,
96+
path: customPath
97+
});
98+
99+
wallet.derivationPath.should.equal(customPath);
100+
});
101+
102+
it('should derive different addresses for different paths', function() {
103+
const path1 = 'm/49\'/60\'/0\'/0/0';
104+
const path2 = 'm/49\'/60\'/0\'/0/1';
105+
106+
const wallet1 = walletFromMnemonic({mnemonic: testMnemonic, path: path1});
107+
const wallet2 = walletFromMnemonic({mnemonic: testMnemonic, path: path2});
108+
109+
wallet1.address.should.not.equal(wallet2.address);
110+
});
111+
112+
it('should throw error for invalid mnemonic', function() {
113+
let error;
114+
try {
115+
walletFromMnemonic({mnemonic: 'invalid mnemonic phrase'});
116+
} catch(e) {
117+
error = e;
118+
}
119+
120+
should.exist(error);
121+
});
122+
123+
it('should include the mnemonic in the returned wallet', function() {
124+
const wallet = walletFromMnemonic({mnemonic: testMnemonic});
125+
wallet.mnemonic.should.equal(testMnemonic);
126+
});
127+
});
128+
129+
describe('Integration: generateMnemonic + walletFromMnemonic', function() {
130+
it('should create a complete wallet from generated mnemonic', function() {
131+
// Generate mnemonic
132+
const mnemonic = generateMnemonic();
133+
134+
// Derive wallet
135+
const wallet = walletFromMnemonic({mnemonic});
136+
137+
// Verify all fields exist
138+
should.exist(wallet.address);
139+
should.exist(wallet.privateKey);
140+
should.exist(wallet.publicKey);
141+
wallet.derivationPath.should.equal('m/49\'/60\'/0\'/0/0');
142+
wallet.mnemonic.should.equal(mnemonic);
143+
144+
// Verify formats
145+
wallet.address.should.match(/^0x[a-fA-F0-9]{40}$/);
146+
wallet.privateKey.should.match(/^0x[a-fA-F0-9]{64}$/);
147+
});
148+
149+
it('should allow wallet recovery from mnemonic', function() {
150+
// Simulate: User creates wallet
151+
const mnemonic = generateMnemonic();
152+
const originalWallet = walletFromMnemonic({mnemonic});
153+
154+
// Simulate: User loses device, recovers with mnemonic
155+
const recoveredWallet = walletFromMnemonic({mnemonic});
156+
157+
// Same mnemonic = same wallet
158+
recoveredWallet.address.should.equal(originalWallet.address);
159+
recoveredWallet.privateKey.should.equal(originalWallet.privateKey);
160+
});
161+
});
162+
});

0 commit comments

Comments
 (0)