@@ -999,7 +999,9 @@ impl OfferContents {
999999 let ( currency, amount) = match & self . amount {
10001000 None => ( None , None ) ,
10011001 Some ( Amount :: Bitcoin { amount_msats } ) => ( None , Some ( * amount_msats) ) ,
1002- Some ( Amount :: Currency { iso4217_code, amount } ) => ( Some ( iso4217_code) , Some ( * amount) ) ,
1002+ Some ( Amount :: Currency { iso4217_code, amount } ) => {
1003+ ( Some ( iso4217_code. as_bytes ( ) ) , Some ( * amount) )
1004+ } ,
10031005 } ;
10041006
10051007 let features = {
@@ -1076,7 +1078,62 @@ pub enum Amount {
10761078}
10771079
10781080/// An ISO 4217 three-letter currency code (e.g., USD).
1079- pub type CurrencyCode = [ u8 ; 3 ] ;
1081+ ///
1082+ /// Currency codes must be exactly 3 ASCII uppercase letters.
1083+ #[ derive( Clone , Copy , Debug , PartialEq , Eq , Hash ) ]
1084+ pub struct CurrencyCode ( [ u8 ; 3 ] ) ;
1085+
1086+ impl CurrencyCode {
1087+ /// Creates a new `CurrencyCode` from a 3-byte array.
1088+ ///
1089+ /// Returns an error if the bytes are not valid UTF-8 or not all ASCII uppercase.
1090+ pub fn new ( code : [ u8 ; 3 ] ) -> Result < Self , CurrencyCodeError > {
1091+ let currency_str =
1092+ core:: str:: from_utf8 ( & code) . map_err ( |_| CurrencyCodeError :: InvalidUtf8 ) ?;
1093+
1094+ if !currency_str. chars ( ) . all ( |c| c. is_ascii_uppercase ( ) ) {
1095+ return Err ( CurrencyCodeError :: NotAsciiUppercase { code : currency_str. to_string ( ) } ) ;
1096+ }
1097+
1098+ Ok ( Self ( code) )
1099+ }
1100+
1101+ /// Returns the currency code as a byte array.
1102+ pub fn as_bytes ( & self ) -> & [ u8 ; 3 ] {
1103+ & self . 0
1104+ }
1105+
1106+ /// Returns the currency code as a string slice.
1107+ pub fn as_str ( & self ) -> & str {
1108+ unsafe { core:: str:: from_utf8_unchecked ( & self . 0 ) }
1109+ }
1110+ }
1111+
1112+ impl FromStr for CurrencyCode {
1113+ type Err = CurrencyCodeError ;
1114+
1115+ fn from_str ( s : & str ) -> Result < Self , Self :: Err > {
1116+ if s. len ( ) != 3 {
1117+ return Err ( CurrencyCodeError :: InvalidLength { actual : s. len ( ) } ) ;
1118+ }
1119+
1120+ let mut code = [ 0u8 ; 3 ] ;
1121+ code. copy_from_slice ( s. as_bytes ( ) ) ;
1122+ Self :: new ( code)
1123+ }
1124+ }
1125+
1126+ impl AsRef < [ u8 ] > for CurrencyCode {
1127+ fn as_ref ( & self ) -> & [ u8 ] {
1128+ & self . 0
1129+ }
1130+ }
1131+
1132+ impl core:: fmt:: Display for CurrencyCode {
1133+ fn fmt ( & self , f : & mut core:: fmt:: Formatter < ' _ > ) -> core:: fmt:: Result {
1134+ f. write_str ( self . as_str ( ) )
1135+ }
1136+ }
10801137
10811138/// Quantity of items supported by an [`Offer`].
10821139#[ derive( Clone , Copy , Debug , PartialEq ) ]
@@ -1115,7 +1172,7 @@ const OFFER_ISSUER_ID_TYPE: u64 = 22;
11151172tlv_stream ! ( OfferTlvStream , OfferTlvStreamRef <' a>, OFFER_TYPES , {
11161173 ( 2 , chains: ( Vec <ChainHash >, WithoutLength ) ) ,
11171174 ( OFFER_METADATA_TYPE , metadata: ( Vec <u8 >, WithoutLength ) ) ,
1118- ( 6 , currency: CurrencyCode ) ,
1175+ ( 6 , currency: [ u8 ; 3 ] ) ,
11191176 ( 8 , amount: ( u64 , HighZeroBytesDroppedBigSize ) ) ,
11201177 ( 10 , description: ( String , WithoutLength ) ) ,
11211178 ( 12 , features: ( OfferFeatures , WithoutLength ) ) ,
@@ -1209,7 +1266,11 @@ impl TryFrom<FullOfferTlvStream> for OfferContents {
12091266 } ,
12101267 ( None , Some ( amount_msats) ) => Some ( Amount :: Bitcoin { amount_msats } ) ,
12111268 ( Some ( _) , None ) => return Err ( Bolt12SemanticError :: MissingAmount ) ,
1212- ( Some ( iso4217_code) , Some ( amount) ) => Some ( Amount :: Currency { iso4217_code, amount } ) ,
1269+ ( Some ( currency_bytes) , Some ( amount) ) => {
1270+ let iso4217_code = CurrencyCode :: new ( currency_bytes)
1271+ . map_err ( |_| Bolt12SemanticError :: InvalidCurrencyCode ) ?;
1272+ Some ( Amount :: Currency { iso4217_code, amount } )
1273+ } ,
12131274 } ;
12141275
12151276 if amount. is_some ( ) && description. is_none ( ) {
@@ -1256,6 +1317,31 @@ impl core::fmt::Display for Offer {
12561317 }
12571318}
12581319
1320+ /// Errors that can occur when creating or parsing a `CurrencyCode`
1321+ #[ derive( Clone , Debug , PartialEq , Eq ) ]
1322+ pub enum CurrencyCodeError {
1323+ /// The currency code must be exactly 3 bytes
1324+ InvalidLength { actual : usize } ,
1325+ /// The currency code contains invalid UTF-8
1326+ InvalidUtf8 ,
1327+ /// The currency code must be all ASCII uppercase letters
1328+ NotAsciiUppercase { code : String } ,
1329+ }
1330+
1331+ impl core:: fmt:: Display for CurrencyCodeError {
1332+ fn fmt ( & self , f : & mut core:: fmt:: Formatter < ' _ > ) -> core:: fmt:: Result {
1333+ match self {
1334+ Self :: InvalidLength { actual } => {
1335+ write ! ( f, "Currency code must be 3 bytes, got {}" , actual)
1336+ } ,
1337+ Self :: InvalidUtf8 => write ! ( f, "Currency code contains invalid UTF-8" ) ,
1338+ Self :: NotAsciiUppercase { code } => {
1339+ write ! ( f, "Currency code '{}' must be all ASCII uppercase" , code)
1340+ } ,
1341+ }
1342+ }
1343+ }
1344+
12591345#[ cfg( test) ]
12601346mod tests {
12611347 #[ cfg( not( c_bindings) ) ]
@@ -1273,6 +1359,7 @@ mod tests {
12731359 use crate :: ln:: inbound_payment:: ExpandedKey ;
12741360 use crate :: ln:: msgs:: { DecodeError , MAX_VALUE_MSAT } ;
12751361 use crate :: offers:: nonce:: Nonce ;
1362+ use crate :: offers:: offer:: CurrencyCode ;
12761363 use crate :: offers:: parse:: { Bolt12ParseError , Bolt12SemanticError } ;
12771364 use crate :: offers:: test_utils:: * ;
12781365 use crate :: types:: features:: OfferFeatures ;
@@ -1541,7 +1628,8 @@ mod tests {
15411628 #[ test]
15421629 fn builds_offer_with_amount ( ) {
15431630 let bitcoin_amount = Amount :: Bitcoin { amount_msats : 1000 } ;
1544- let currency_amount = Amount :: Currency { iso4217_code : * b"USD" , amount : 10 } ;
1631+ let currency_amount =
1632+ Amount :: Currency { iso4217_code : CurrencyCode :: new ( * b"USD" ) . unwrap ( ) , amount : 10 } ;
15451633
15461634 let offer = OfferBuilder :: new ( pubkey ( 42 ) ) . amount_msats ( 1000 ) . build ( ) . unwrap ( ) ;
15471635 let tlv_stream = offer. as_tlv_stream ( ) ;
@@ -1820,6 +1908,36 @@ mod tests {
18201908 Bolt12ParseError :: InvalidSemantics ( Bolt12SemanticError :: InvalidAmount )
18211909 ) ,
18221910 }
1911+
1912+ let mut tlv_stream = offer. as_tlv_stream ( ) ;
1913+ tlv_stream. 0 . amount = Some ( 1000 ) ;
1914+ tlv_stream. 0 . currency = Some ( b"\xFF \xFE \xFD " ) ; // invalid UTF-8 bytes
1915+
1916+ let mut encoded_offer = Vec :: new ( ) ;
1917+ tlv_stream. write ( & mut encoded_offer) . unwrap ( ) ;
1918+
1919+ match Offer :: try_from ( encoded_offer) {
1920+ Ok ( _) => panic ! ( "expected error" ) ,
1921+ Err ( e) => assert_eq ! (
1922+ e,
1923+ Bolt12ParseError :: InvalidSemantics ( Bolt12SemanticError :: InvalidCurrencyCode )
1924+ ) ,
1925+ }
1926+
1927+ let mut tlv_stream = offer. as_tlv_stream ( ) ;
1928+ tlv_stream. 0 . amount = Some ( 1000 ) ;
1929+ tlv_stream. 0 . currency = Some ( b"usd" ) ; // invalid ISO 4217 code
1930+
1931+ let mut encoded_offer = Vec :: new ( ) ;
1932+ tlv_stream. write ( & mut encoded_offer) . unwrap ( ) ;
1933+
1934+ match Offer :: try_from ( encoded_offer) {
1935+ Ok ( _) => panic ! ( "expected error" ) ,
1936+ Err ( e) => assert_eq ! (
1937+ e,
1938+ Bolt12ParseError :: InvalidSemantics ( Bolt12SemanticError :: InvalidCurrencyCode )
1939+ ) ,
1940+ }
18231941 }
18241942
18251943 #[ test]
0 commit comments