11use cgka_traits:: MemberId ;
2+ use nostr:: FromBech32 ;
23use nostr:: ToBech32 ;
34use nostr:: nips:: nip19:: Nip19Profile ;
45use nostr:: prelude:: RelayUrl ;
@@ -78,12 +79,22 @@ pub fn nprofile_for_account_id(
7879 . map_err ( |_| AppError :: InvalidPublicKey )
7980}
8081
81- /// Normalize any public-key reference (npub bech32 or hex) into canonical
82- /// hex account id. Public so embedders can resolve scanned/typed npubs.
82+ /// Normalize a public identity reference into a canonical hex account id.
83+ ///
84+ /// Accepts hex, `npub`, one lowercase `nostr:npub` prefix, bare `nprofile`,
85+ /// and one lowercase `nostr:nprofile` prefix. Existing hex/npub/NIP-21
86+ /// behavior stays on [`PublicKey::parse`]; nprofile is a fallback that
87+ /// returns only `profile.public_key` hex and discards every relay hint.
88+ /// This helper does not trim whitespace. Failures map to
89+ /// [`AppError::InvalidPublicKey`] without echoing the input.
8390pub fn account_id_hex_from_ref ( reference : & str ) -> Result < String , AppError > {
84- Ok ( PublicKey :: parse ( reference)
85- . map_err ( |_| AppError :: InvalidPublicKey ) ?
86- . to_hex ( ) )
91+ if let Ok ( pubkey) = PublicKey :: parse ( reference) {
92+ return Ok ( pubkey. to_hex ( ) ) ;
93+ }
94+ let profile_ref = reference. strip_prefix ( "nostr:" ) . unwrap_or ( reference) ;
95+ Nip19Profile :: from_bech32 ( profile_ref)
96+ . map ( |profile| profile. public_key . to_hex ( ) )
97+ . map_err ( |_| AppError :: InvalidPublicKey )
8798}
8899
89100pub ( crate ) fn npub_for_account_id_lossy ( account_id_hex : & str ) -> String {
@@ -92,17 +103,54 @@ pub(crate) fn npub_for_account_id_lossy(account_id_hex: &str) -> String {
92103
93104#[ cfg( test) ]
94105mod tests {
95- use super :: { nprofile_for_account_id, npub_for_account_id} ;
106+ use super :: {
107+ account_id_hex_from_ref, nprofile_for_account_id, npub_for_account_id, parse_account_id_hex,
108+ } ;
109+ use crate :: AppError ;
110+ use nostr:: nips:: nip01:: Coordinate ;
111+ use nostr:: nips:: nip19:: { Nip19Coordinate , Nip19Event } ;
112+ use nostr:: { EventId , Kind , SecretKey , ToBech32 } ;
113+ use nostr_sdk:: prelude:: PublicKey ;
96114
97115 const ACCOUNT_ID : & str = "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4" ;
116+ const NPUB : & str = "npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy" ;
117+ const BOOTSTRAP_NPROFILE : & str = "nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dqpremhxue69uhhyetvv9uju\
118+ et49emks6t5v4hx76tnv5hxx6rpwsq3uamnwvaz7tmjv4kxz7fww4ejuamgd96x2mn0d9ek2tnrdpshggcu28s";
119+ const NO_RELAY_NPROFILE : & str =
120+ "nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dq7r0nz9" ;
121+ const UNKNOWN_TLV_NPROFILE : & str =
122+ "nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0drrq3skycmyxne9kf" ;
123+ const INVALID_RELAY_NPROFILE : & str =
124+ "nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dqpp9hx7apqvys82unvuca0cf" ;
125+ const DUPLICATE_TYPE0_NPROFILE : & str = "nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dqqyzamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamkq4uk7z" ;
126+ const MISSING_TYPE0_NPROFILE : & str = "nprofile1qy2hwumn8ghj7etcv9khqmr99e5kuanpd35kghsdudn" ;
127+ const SHORT_TYPE0_NPROFILE : & str = "nprofile1qqg25n7gve04d9hr8km7rftjuwc028ngcqe" ;
128+ const TRUNCATED_HEADER_NPROFILE : & str = "nprofile1qqqsnhxh" ;
129+ const TRUNCATED_VALUE_NPROFILE : & str = "nprofile1qqs25n7gve04d9hrdfl42d" ;
130+ const WRONG_HRP_NOTE : & str =
131+ "note1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dq4ueyyt" ;
132+
133+ fn assert_account_id ( reference : & str ) {
134+ assert_eq ! (
135+ account_id_hex_from_ref( reference) . expect( "valid identity reference" ) ,
136+ ACCOUNT_ID
137+ ) ;
138+ }
139+
140+ fn assert_invalid ( reference : & str ) {
141+ assert ! (
142+ matches!(
143+ account_id_hex_from_ref( reference) ,
144+ Err ( AppError :: InvalidPublicKey )
145+ ) ,
146+ "expected InvalidPublicKey"
147+ ) ;
148+ }
98149
99150 #[ test]
100151 fn npub_and_nprofile_match_bootstrap_vectors ( ) {
101152 let npub = npub_for_account_id ( ACCOUNT_ID ) . unwrap ( ) ;
102- assert_eq ! (
103- npub,
104- "npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy"
105- ) ;
153+ assert_eq ! ( npub, NPUB ) ;
106154
107155 let nprofile = nprofile_for_account_id (
108156 ACCOUNT_ID ,
@@ -112,10 +160,100 @@ mod tests {
112160 ] ,
113161 )
114162 . unwrap ( ) ;
115- assert_eq ! (
116- nprofile,
117- "nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dqpremhxue69uhhyetvv9uju\
118- et49emks6t5v4hx76tnv5hxx6rpwsq3uamnwvaz7tmjv4kxz7fww4ejuamgd96x2mn0d9ek2tnrdpshggcu28s"
163+ assert_eq ! ( nprofile, BOOTSTRAP_NPROFILE ) ;
164+ }
165+
166+ #[ test]
167+ fn account_id_hex_from_ref_accepts_hex_npub_and_nprofile_spellings ( ) {
168+ let no_relay = nprofile_for_account_id ( ACCOUNT_ID , & [ ] ) . unwrap ( ) ;
169+ assert_eq ! ( no_relay, NO_RELAY_NPROFILE ) ;
170+ let other_relays =
171+ nprofile_for_account_id ( ACCOUNT_ID , & [ "wss://relay.example.invalid" . to_owned ( ) ] )
172+ . unwrap ( ) ;
173+ assert_ne ! ( other_relays, BOOTSTRAP_NPROFILE ) ;
174+
175+ for reference in [
176+ ACCOUNT_ID ,
177+ & ACCOUNT_ID . to_ascii_uppercase ( ) ,
178+ NPUB ,
179+ & format ! ( "nostr:{NPUB}" ) ,
180+ BOOTSTRAP_NPROFILE ,
181+ & format ! ( "nostr:{BOOTSTRAP_NPROFILE}" ) ,
182+ NO_RELAY_NPROFILE ,
183+ & format ! ( "nostr:{NO_RELAY_NPROFILE}" ) ,
184+ & other_relays,
185+ UNKNOWN_TLV_NPROFILE ,
186+ INVALID_RELAY_NPROFILE ,
187+ DUPLICATE_TYPE0_NPROFILE ,
188+ ] {
189+ assert_account_id ( reference) ;
190+ }
191+ }
192+
193+ #[ test]
194+ fn account_id_hex_from_ref_keeps_untrimmed_whitespace_behavior ( ) {
195+ assert_invalid ( & format ! ( " {ACCOUNT_ID}" ) ) ;
196+ assert_invalid ( & format ! ( "{NPUB} " ) ) ;
197+ assert_invalid ( & format ! ( " {BOOTSTRAP_NPROFILE}" ) ) ;
198+ }
199+
200+ #[ test]
201+ fn account_id_hex_from_ref_rejects_checksum_damage_and_wrong_hrp ( ) {
202+ let mut damaged_npub = NPUB . to_owned ( ) ;
203+ damaged_npub. replace_range ( damaged_npub. len ( ) - 1 .., "x" ) ;
204+ assert_ne ! ( damaged_npub, NPUB ) ;
205+ assert_invalid ( & damaged_npub) ;
206+
207+ let mut damaged_nprofile = NO_RELAY_NPROFILE . to_owned ( ) ;
208+ damaged_nprofile. replace_range ( damaged_nprofile. len ( ) - 1 .., "x" ) ;
209+ assert_ne ! ( damaged_nprofile, NO_RELAY_NPROFILE ) ;
210+ assert_invalid ( & damaged_nprofile) ;
211+
212+ assert_invalid ( WRONG_HRP_NOTE ) ;
213+ assert_invalid (
214+ "NOSTR:nprofile1qqs25n7gve04d9hr8km7rftjuwc0tv7kzkphkrek9h93eqrgkzvv0dq7r0nz9" ,
119215 ) ;
216+ assert_invalid ( & format ! ( "nostr:nostr:{NO_RELAY_NPROFILE}" ) ) ;
217+ }
218+
219+ #[ test]
220+ fn account_id_hex_from_ref_rejects_event_and_secret_hrps ( ) {
221+ let public_key = PublicKey :: parse ( ACCOUNT_ID ) . unwrap ( ) ;
222+ let event_id = EventId :: from_slice ( & [ 0x22 ; 32 ] ) . unwrap ( ) ;
223+ let note = event_id. to_bech32 ( ) . unwrap ( ) ;
224+ let nevent = Nip19Event :: new ( event_id)
225+ . author ( public_key)
226+ . to_bech32 ( )
227+ . unwrap ( ) ;
228+ let naddr = Nip19Coordinate :: new ( Coordinate :: new ( Kind :: Metadata , public_key) , [ ] )
229+ . to_bech32 ( )
230+ . unwrap ( ) ;
231+ let nsec = SecretKey :: from_slice ( & [ 0x11 ; 32 ] )
232+ . unwrap ( )
233+ . to_bech32 ( )
234+ . unwrap ( ) ;
235+
236+ assert_invalid ( & note) ;
237+ assert_invalid ( & nevent) ;
238+ assert_invalid ( & naddr) ;
239+ assert_invalid ( & nsec) ;
240+ }
241+
242+ #[ test]
243+ fn account_id_hex_from_ref_rejects_truncated_and_missing_type0 ( ) {
244+ assert_invalid ( MISSING_TYPE0_NPROFILE ) ;
245+ assert_invalid ( SHORT_TYPE0_NPROFILE ) ;
246+ assert_invalid ( TRUNCATED_HEADER_NPROFILE ) ;
247+ assert_invalid ( TRUNCATED_VALUE_NPROFILE ) ;
248+ }
249+
250+ #[ test]
251+ fn parse_account_id_hex_does_not_gain_nprofile ( ) {
252+ assert_eq ! ( parse_account_id_hex( ACCOUNT_ID ) . unwrap( ) , ACCOUNT_ID ) ;
253+ assert_eq ! ( parse_account_id_hex( NPUB ) . unwrap( ) , ACCOUNT_ID ) ;
254+ assert ! ( matches!(
255+ parse_account_id_hex( NO_RELAY_NPROFILE ) ,
256+ Err ( AppError :: InvalidPublicKey )
257+ ) ) ;
120258 }
121259}
0 commit comments