Skip to content

Commit 5d6c939

Browse files
Merge pull request #483 from autonomys/eth-key-derivation
feat: add optional derivationPath for Ethereum HD wallets
2 parents f7133cf + e05e120 commit 5d6c939

File tree

8 files changed

+162
-10
lines changed

8 files changed

+162
-10
lines changed

examples/node/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,33 @@ yarn autoid:view-revoked-certs <AUTO_ID_IDENTIFIER>
5959

6060
```bash
6161
yarn address
62+
yarn eth-derivation-demo
63+
```
64+
65+
### Ethereum derivation paths (demo)
66+
67+
The `eth-derivation-demo` script shows the difference between deriving an Ethereum address from the master key (`m`) and from the BIP44 path (`m/44'/60'/0'/0/0`). It also cross-checks results using `ethers`.
68+
69+
To run:
70+
71+
```bash
72+
yarn eth-derivation-demo
73+
```
74+
75+
Programmatic usage with optional `derivationPath`:
76+
77+
```ts
78+
import { setupWallet } from '@autonomys/auto-utils'
79+
80+
// Master key (m) derivation
81+
const ethMaster = setupWallet({ mnemonic, type: 'ethereum' })
82+
83+
// BIP44 derivation
84+
const ethBip44 = setupWallet({
85+
mnemonic,
86+
type: 'ethereum',
87+
derivationPath: "m/44'/60'/0'/0/0",
88+
} as any)
6289
```
6390

6491
## Run All

examples/node/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"cli": "npx ts-node ./src/cli.ts",
1717
"all": "yarn address && yarn balance && yarn transfer && yarn operators && yarn register-operator && yarn nominate-operator && yarn withdraw-stake && yarn deregister-operator",
1818
"address": "npx ts-node ./src/address.ts",
19+
"eth-derivation-demo": "npx ts-node ./src/eth-derivation-demo.ts",
1920
"balance": "npx ts-node ./src/balance.ts",
2021
"transfer": "npx ts-node ./src/transfer.ts",
2122
"operators": "npx ts-node ./src/operators.ts",
@@ -28,7 +29,8 @@
2829
},
2930
"dependencies": {
3031
"@autonomys/auto-consensus": "workspace:*",
31-
"@autonomys/auto-utils": "workspace:*"
32+
"@autonomys/auto-utils": "workspace:*",
33+
"ethers": "^6.13.5"
3234
},
3335
"devDependencies": {
3436
"commander": "^12.1.0",
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { setupWallet } from '@autonomys/auto-utils'
2+
import { HDNodeWallet, Mnemonic, Wallet } from 'ethers'
3+
4+
const MNEMONIC = 'test test test test test test test test test test test junk'
5+
6+
const main = async (): Promise<void> => {
7+
const master = setupWallet({ mnemonic: MNEMONIC, type: 'ethereum' })
8+
9+
const bip44Path = "m/44'/60'/0'/0/0"
10+
const bip44 = setupWallet({
11+
mnemonic: MNEMONIC,
12+
type: 'ethereum',
13+
derivationPath: bip44Path,
14+
})
15+
16+
const ethersMnemonic = Mnemonic.fromPhrase(MNEMONIC)
17+
const ethersWallet = Wallet.fromPhrase(MNEMONIC)
18+
const ethersMaster = HDNodeWallet.fromMnemonic(ethersMnemonic, 'm')
19+
const ethersBip44 = HDNodeWallet.fromMnemonic(ethersMnemonic, bip44Path)
20+
21+
console.log('Mnemonic:', MNEMONIC)
22+
console.log('Master (m) address:', master.address)
23+
console.log(`BIP44 (${bip44Path}) address:`, bip44.address)
24+
console.log('Ethers Wallet address:', ethersWallet.address)
25+
console.log('Ethers Master (m) address:', ethersMaster.address)
26+
console.log(`Ethers BIP44 (${bip44Path}) address:`, ethersBip44.address)
27+
console.log('Match?', master.address.toLowerCase() === bip44.address.toLowerCase())
28+
console.log(
29+
'Auto (m) === Ethers (m)?',
30+
master.address.toLowerCase() === ethersMaster.address.toLowerCase(),
31+
)
32+
console.log(
33+
`Auto BIP44 === Ethers BIP44?`,
34+
bip44.address.toLowerCase() === ethersBip44.address.toLowerCase(),
35+
)
36+
console.log(
37+
'Note: Current SDK behavior derives Ethereum from master key (m) unless a path is provided.',
38+
)
39+
}
40+
41+
main().catch((error) => {
42+
console.error(error)
43+
process.exit(1)
44+
})

packages/auto-utils/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ import { activateWallet } from '@autonomys/auto-utils'
7171
const { api, accounts } = await activateWallet({
7272
mnemonic,
7373
networkId: 'mainnet', // Optional: specify the network ID
74+
// Optional derivation path (useful for ethereum BIP44):
75+
// derivationPath: "m/44'/60'/0'/0/0",
7476
})
7577

7678
const account = accounts[0]
@@ -263,6 +265,27 @@ import { activateDomain } from '@autonomys/auto-utils'
263265

264266
---
265267

268+
#### **Setup a Wallet with Ethereum BIP44**
269+
270+
```typescript
271+
import { setupWallet } from '@autonomys/auto-utils'
272+
273+
const mnemonic = 'your mnemonic phrase here'
274+
275+
// Default behavior derives from master (m)
276+
const ethMaster = setupWallet({ mnemonic, type: 'ethereum' })
277+
278+
// To derive using BIP44 path
279+
const ethBip44 = setupWallet({
280+
mnemonic,
281+
type: 'ethereum',
282+
derivationPath: "m/44'/60'/0'/0/0",
283+
})
284+
285+
console.log(ethMaster.address)
286+
console.log(ethBip44.address)
287+
```
288+
266289
### 5. Address Utilities
267290

268291
#### **Convert Address Formats**

packages/auto-utils/__test__/wallet.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('Verify wallet functions', () => {
2424
const TEST_MNEMONIC = 'test test test test test test test test test test test junk'
2525
const TEST_ADDRESS = '5GmS1wtCfR4tK5SSgnZbVT4kYw5W8NmxmijcsxCQE6oLW6A8'
2626
const TEST_ADDRESS_ETHEREUM = '0xF5a6EAD936fb47f342Bb63E676479bDdf26EbE1d'
27+
const TEST_ADDRESS_ETHEREUM_BIP44 = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
2728
const ALICE_URI = '//Alice'
2829
const BOB_URI = '//Bob'
2930
let api: ApiPromise
@@ -63,6 +64,17 @@ describe('Verify wallet functions', () => {
6364
expect(wallet.address).toEqual(TEST_ADDRESS_ETHEREUM)
6465
})
6566

67+
test("Check setupWallet returns expected address when provided with Ethereum BIP44 derivationPath ('m/44'/60'/0'/0/0')", async () => {
68+
const wallet = setupWallet({
69+
mnemonic: TEST_MNEMONIC,
70+
type: 'ethereum',
71+
derivationPath: "m/44'/60'/0'/0/0",
72+
} as any)
73+
expect(wallet.keyringPair?.type).toEqual('ethereum')
74+
expect(wallet.address.startsWith('0x')).toBeTruthy()
75+
expect(wallet.address).toEqual(TEST_ADDRESS_ETHEREUM_BIP44)
76+
})
77+
6678
test('Check setupWallet return a pair with matching private key when provided with Alice seed', async () => {
6779
const wallet = setupWallet({ uri: ALICE_URI })
6880
expect(wallet.commonAddress).toEqual(aliceWallet.address)
@@ -112,6 +124,19 @@ describe('Verify wallet functions', () => {
112124
expect(accounts[0].address).toEqual(TEST_ADDRESS_ETHEREUM)
113125
}, 15000)
114126

127+
test('Check activateWallet returns an account with expected address when provided with a test mnemonic (ethereum + BIP44 derivationPath)', async () => {
128+
const { api, accounts } = await activateWallet({
129+
...TEST_NETWORK,
130+
mnemonic: TEST_MNEMONIC,
131+
type: 'ethereum',
132+
derivationPath: "m/44'/60'/0'/0/0",
133+
} as ActivateWalletParams)
134+
expect(api).toBeDefined()
135+
expect(accounts.length).toBeGreaterThan(0)
136+
expect(accounts[0].address.startsWith('0x')).toBeTruthy()
137+
expect(accounts[0].address).toEqual(TEST_ADDRESS_ETHEREUM_BIP44)
138+
}, 15000)
139+
115140
test('Check activateWallet return an api instance and an account when provided with Alice uri', async () => {
116141
const { api, accounts } = await activateWallet({
117142
...TEST_NETWORK,

packages/auto-utils/src/types/wallet.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ export type WalletType = {
2626
type?: KeypairType
2727
}
2828

29-
export type SetupWalletParams = MnemonicOrURI & WalletType
29+
export type SetupWalletParams = MnemonicOrURI &
30+
WalletType & {
31+
derivationPath?: string
32+
}
3033

3134
export type Wallet = {
3235
keyringPair?: KeyringPair
@@ -42,7 +45,9 @@ export interface GeneratedWallet extends Wallet {
4245
export type ActivateWalletParams = (NetworkParams | DomainParams) &
4346
MnemonicOrURI &
4447
ExtraActivationOptions &
45-
WalletType
48+
WalletType & {
49+
derivationPath?: string
50+
}
4651

4752
export type WalletActivated = {
4853
api: ApiPromise

packages/auto-utils/src/wallet.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ import type {
4646
* })
4747
* console.log(ethWallet.address) // Ethereum-format address (0x...)
4848
*
49+
* // Setup ethereum wallet with BIP44 derivation path
50+
* const ethBip44 = setupWallet({
51+
* mnemonic: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
52+
* type: 'ethereum',
53+
* derivationPath: "m/44'/60'/0'/0/0",
54+
* })
55+
* console.log(ethBip44.address) // Matches MetaMask default derivation
56+
*
4957
* // Setup development wallet from URI
5058
* const aliceWallet = setupWallet({ uri: '//Alice' })
5159
* console.log(aliceWallet.address) // Alice's development address
@@ -67,7 +75,9 @@ export const setupWallet = (params: SetupWalletParams): Wallet => {
6775
keyringPair = keyring.addFromUri((params as URI).uri)
6876
} else if ((params as Mnemonic).mnemonic) {
6977
// Treat as mnemonic
70-
keyringPair = keyring.addFromUri((params as Mnemonic).mnemonic)
78+
const base = (params as Mnemonic).mnemonic
79+
const withPath = params.derivationPath ? `${base}/${params.derivationPath}` : base
80+
keyringPair = keyring.addFromUri(withPath)
7181
} else throw new Error('Invalid mnemonic or private key')
7282

7383
return {
@@ -140,22 +150,22 @@ export const generateWallet = (type: KeypairType = 'sr25519'): GeneratedWallet =
140150
* console.log('Account address:', accounts[0].address)
141151
*
142152
* // Activate on specific network
143-
* const { api: taurusApi, accounts: taurusAccounts } = await activateWallet({
153+
* const { api: mainnetApi, accounts: mainnetAccounts } = await activateWallet({
144154
* mnemonic: 'your mnemonic here',
145-
* networkId: 'taurus'
155+
* networkId: 'mainnet'
146156
* })
147157
*
148158
* // Activate on domain
149159
* const { api: domainApi, accounts: domainAccounts } = await activateWallet({
150160
* uri: '//Alice',
151-
* networkId: 'taurus',
161+
* networkId: 'mainnet',
152162
* domainId: '0' // Auto-EVM domain
153163
* })
154164
*
155165
* // Activate with ethereum key type
156166
* const { api: ethApi, accounts: ethAccounts } = await activateWallet({
157167
* mnemonic: 'your mnemonic here',
158-
* networkId: 'taurus',
168+
* networkId: 'mainnet',
159169
* type: 'ethereum'
160170
* })
161171
*
@@ -229,15 +239,15 @@ export const activateWallet = async (params: ActivateWalletParams): Promise<Wall
229239
* console.log('Created', wallets.length, 'mock wallets')
230240
*
231241
* // Create mock wallets for testnet
232-
* const testWallets = await mockWallets({ networkId: 'taurus' })
242+
* const testWallets = await mockWallets({ networkId: 'mainnet' })
233243
*
234244
* // Create mock wallets with existing API
235245
* const api = await activate({ networkId: 'localhost' })
236246
* const localWallets = await mockWallets({ networkId: 'localhost' }, api)
237247
*
238248
* // Create ethereum-type mock wallets
239249
* const ethWallets = await mockWallets(
240-
* { networkId: 'taurus' },
250+
* { networkId: 'mainnet' },
241251
* undefined,
242252
* 'ethereum'
243253
* )

yarn.lock

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13532,6 +13532,21 @@ __metadata:
1353213532
languageName: node
1353313533
linkType: hard
1353413534

13535+
"ethers@npm:^6.13.5":
13536+
version: 6.15.0
13537+
resolution: "ethers@npm:6.15.0"
13538+
dependencies:
13539+
"@adraffy/ens-normalize": "npm:1.10.1"
13540+
"@noble/curves": "npm:1.2.0"
13541+
"@noble/hashes": "npm:1.3.2"
13542+
"@types/node": "npm:22.7.5"
13543+
aes-js: "npm:4.0.0-beta.5"
13544+
tslib: "npm:2.7.0"
13545+
ws: "npm:8.17.1"
13546+
checksum: 10c0/0a4581b662fe46a889a524d3aba43dc6f0ac59b3ae08dce678ee4b5799aab4906109ab24684c9644deedfc9d6e79b59faccecbeda9b6b7ceb085724d596a49e9
13547+
languageName: node
13548+
linkType: hard
13549+
1353513550
"event-emitter@npm:^0.3.5":
1353613551
version: 0.3.5
1353713552
resolution: "event-emitter@npm:0.3.5"
@@ -18499,6 +18514,7 @@ __metadata:
1849918514
"@autonomys/auto-utils": "workspace:*"
1850018515
commander: "npm:^12.1.0"
1850118516
dotenv: "npm:^16.4.5"
18517+
ethers: "npm:^6.13.5"
1850218518
languageName: unknown
1850318519
linkType: soft
1850418520

0 commit comments

Comments
 (0)