@@ -252,15 +252,15 @@ impl CurrencyUnit {
252252
253253 /// Generate deterministic derivation index for custom currency units
254254 fn custom_derivation_index ( s : & str , reserved : u32 ) -> Option < u32 > {
255- // 1) NFC normalization: composes equivalent Unicode sequences (e.g., "e" + U+0301) into a single
256- // canonical code point (e.g., "é") so visually/equivalently identical strings hash the same
257- // 2) lowercase: avoids case-induced divergence ("USD" vs "usd")
258- // 3) trim: removes accidental leading/trailing whitespace (" usd " vs "usd")
259- let norm = s. nfc ( ) . collect :: < String > ( ) . to_lowercase ( ) ;
260- let norm = norm . trim ( ) ;
255+ // Canonicalize before hashing to ensure identical representations converge:
256+ // 1) trim ASCII whitespace so " usd " == "usd"
257+ // 2) NFC normalization so composed/decomposed Unicode forms align
258+ // 3) uppercase to guarantee case-insensitive matching while preserving spec intent
259+ let trimmed = s. trim_matches ( | c : char | matches ! ( c , ' ' | '\t' | '\r' | '\n' ) ) ;
260+ let canonical = trimmed . nfc ( ) . collect :: < String > ( ) . to_uppercase ( ) ;
261261
262262 // use SHA-256 so that the same normalized string always yields the same digest
263- let digest = Sha256 :: hash ( norm . as_bytes ( ) ) ;
263+ let digest = Sha256 :: hash ( canonical . as_bytes ( ) ) ;
264264
265265 // take 4 bytes in a fixed endianness to get a u32
266266 let x = u32:: from_be_bytes ( [ digest[ 0 ] , digest[ 1 ] , digest[ 2 ] , digest[ 3 ] ] ) as u64 ;
@@ -425,7 +425,7 @@ mod tests {
425425 . derivation_index ( )
426426 . expect ( "custom units should always produce an index" ) ;
427427
428- assert_eq ! ( idx, 278_131_397 ) ;
428+ assert_eq ! ( idx, 1_502_388_632 ) ;
429429 }
430430
431431 #[ cfg( all( test, feature = "mint" ) ) ]
@@ -438,4 +438,31 @@ mod tests {
438438 let hardened_max = ( 1 << 31 ) - 1 ;
439439 assert ! ( idx >= 5 && idx <= hardened_max) ;
440440 }
441+
442+ #[ cfg( all( test, feature = "mint" ) ) ]
443+ #[ test]
444+ fn currency_unit_derivation_matches_nut_xx_vectors ( ) {
445+ let vectors = [
446+ ( "sat" , 0 ) ,
447+ ( "msat" , 1 ) ,
448+ ( "auth" , 4 ) ,
449+ ( "usd" , 2 ) ,
450+ ( "eur" , 3 ) ,
451+ ( "nuts" , 1_502_388_632 ) ,
452+ ( " NUTS " , 1_502_388_632 ) ,
453+ ( "eurc" , 1_321_886_555 ) ,
454+ ( "cafe\u{0301} " , 642_348_970 ) ,
455+ ( "CAFÉ" , 642_348_970 ) ,
456+ ( "gbp" , 1_076_107_758 ) ,
457+ ( "JPY" , 1_137_986_283 ) ,
458+ ] ;
459+
460+ for ( input, expected) in vectors {
461+ let unit = CurrencyUnit :: from_str ( input) . unwrap ( ) ;
462+ let idx = unit
463+ . derivation_index ( )
464+ . expect ( "reserved and custom units should yield indices" ) ;
465+ assert_eq ! ( idx, expected, "input {input}" ) ;
466+ }
467+ }
441468}
0 commit comments