@@ -3,6 +3,11 @@ import { describe, expect, test } from "bun:test";
33
44import { KeyPair } from "./KeyPair.ts" ;
55
6+ // Test mnemonic from Solana cookbook:
7+ // https://solana.com/developers/cookbook/wallets/restore-from-mnemonic
8+ const TEST_MNEMONIC =
9+ "neither lonely flavor argue grass remind eye tag avocado spot unusual intact" ;
10+
611describe ( "generate" , ( ) => {
712 test ( "generates a valid keypair with 24-word mnemonic" , async ( ) => {
813 const { keyPair, mnemonic } = await KeyPair . generate ( 24 ) ;
@@ -43,6 +48,112 @@ describe("fromSeedPhrase", () => {
4348 const kpB = await KeyPair . fromSeedPhrase ( b . mnemonic ) ;
4449 expect ( kpA . publicKey ) . not . toEqual ( kpB . publicKey ) ;
4550 } ) ;
51+
52+ test ( "default derivation path matches explicit m/44'/501'/0'/0'" , async ( ) => {
53+ const { mnemonic } = await KeyPair . generate ( ) ;
54+ const defaultPath = await KeyPair . fromSeedPhrase ( mnemonic ) ;
55+ const explicitPath = await KeyPair . fromSeedPhrase (
56+ mnemonic ,
57+ "m/44'/501'/0'/0'"
58+ ) ;
59+ expect ( defaultPath . privateKey ) . toEqual ( explicitPath . privateKey ) ;
60+ expect ( defaultPath . publicKey ) . toEqual ( explicitPath . publicKey ) ;
61+ } ) ;
62+
63+ test ( "custom derivation path is deterministic" , async ( ) => {
64+ const { mnemonic } = await KeyPair . generate ( ) ;
65+ const path = "m/44'/501'/2'/0'" ;
66+ const a = await KeyPair . fromSeedPhrase ( mnemonic , path ) ;
67+ const b = await KeyPair . fromSeedPhrase ( mnemonic , path ) ;
68+ expect ( a . privateKey ) . toEqual ( b . privateKey ) ;
69+ expect ( a . publicKey ) . toEqual ( b . publicKey ) ;
70+ } ) ;
71+ } ) ;
72+
73+ describe ( "fromSeedPhrase — uses SLIP10 for all-hardened paths" , ( ) => {
74+ const toAddress = ( kp : KeyPair ) =>
75+ getBase58Decoder ( ) . decode ( kp . publicKey ) ;
76+
77+ test ( "derives correct addresses for m/44'/501'/x'/0'" , async ( ) => {
78+ for ( const { index, address } of [
79+ { index : 0 , address : "5vftMkHL72JaJG6ExQfGAsT2uGVHpRR7oTNUPMs68Y2N" } ,
80+ { index : 6 , address : "BNMDY3tCyYbayMzBjZm8RW59unpDWcQRfVmWXCJhLb7D" } ,
81+ { index : 9 , address : "6frdqXQAgJMyKwmZxkLYbdGjnYTvUceh6LNhkQt2siQp" } ,
82+ ] ) {
83+ const kp = await KeyPair . fromSeedPhrase (
84+ TEST_MNEMONIC ,
85+ `m/44'/501'/${ index } '/0'`
86+ ) ;
87+ expect ( toAddress ( kp ) ) . toBe ( address ) ;
88+ }
89+ } ) ;
90+
91+ test ( "derives correct addresses for m/44'/501'/x'" , async ( ) => {
92+ for ( const { index, address } of [
93+ { index : 0 , address : "ZtSqp8BQkKFvahawCS9Mf15gzFuedeWWDkYap3qQEe4" } ,
94+ { index : 2 , address : "7Y9pZgwuas2FbVqGKi5yPBKnxeCNxQK8EHCaS3EbXbg8" } ,
95+ { index : 3 , address : "7HWpiMRzpi2BQVVBWQLtSi6yYrSAEJxwAYE1jfYmytan" } ,
96+ ] ) {
97+ const kp = await KeyPair . fromSeedPhrase (
98+ TEST_MNEMONIC ,
99+ `m/44'/501'/${ index } '`
100+ ) ;
101+ expect ( toAddress ( kp ) ) . toBe ( address ) ;
102+ }
103+ } ) ;
104+
105+ test ( "different account indices derive different keypairs" , async ( ) => {
106+ const a = await KeyPair . fromSeedPhrase (
107+ TEST_MNEMONIC ,
108+ "m/44'/501'/0'/0'"
109+ ) ;
110+ const b = await KeyPair . fromSeedPhrase (
111+ TEST_MNEMONIC ,
112+ "m/44'/501'/1'/0'"
113+ ) ;
114+ expect ( a . publicKey ) . not . toEqual ( b . publicKey ) ;
115+ expect ( a . privateKey ) . not . toEqual ( b . privateKey ) ;
116+ } ) ;
117+ } ) ;
118+
119+ describe ( "fromSeedPhrase — uses BIP32 for non-hardened paths" , ( ) => {
120+ const toAddress = ( kp : KeyPair ) =>
121+ getBase58Decoder ( ) . decode ( kp . publicKey ) ;
122+
123+ test ( "derives correct address for m/44'/501'/0'/0/0" , async ( ) => {
124+ const kp = await KeyPair . fromSeedPhrase (
125+ "flee artwork post brown april bulk wash limb melody zoo rib law" ,
126+ "m/44'/501'/0'/0/0"
127+ ) ;
128+ expect ( toAddress ( kp ) ) . toBe (
129+ "F8oiKU5wmZs8jZQng1zyzbcHPkvECSwHitFJvuc5rQGP"
130+ ) ;
131+ } ) ;
132+
133+ test ( "same non-hardened path is deterministic" , async ( ) => {
134+ const a = await KeyPair . fromSeedPhrase (
135+ TEST_MNEMONIC ,
136+ "m/44'/501'/0'/0"
137+ ) ;
138+ const b = await KeyPair . fromSeedPhrase (
139+ TEST_MNEMONIC ,
140+ "m/44'/501'/0'/0"
141+ ) ;
142+ expect ( a . privateKey ) . toEqual ( b . privateKey ) ;
143+ expect ( a . publicKey ) . toEqual ( b . publicKey ) ;
144+ } ) ;
145+
146+ test ( "trailing hardened vs non-hardened segment derive different keypairs" , async ( ) => {
147+ const hardened = await KeyPair . fromSeedPhrase (
148+ TEST_MNEMONIC ,
149+ "m/44'/501'/0'/0'"
150+ ) ;
151+ const nonHardened = await KeyPair . fromSeedPhrase (
152+ TEST_MNEMONIC ,
153+ "m/44'/501'/0'/0"
154+ ) ;
155+ expect ( hardened . publicKey ) . not . toEqual ( nonHardened . publicKey ) ;
156+ } ) ;
46157} ) ;
47158
48159describe ( "fromPrivateKey" , ( ) => {
0 commit comments