@@ -999,7 +999,9 @@ impl OfferContents {
999
999
let ( currency, amount) = match & self . amount {
1000
1000
None => ( None , None ) ,
1001
1001
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
+ } ,
1003
1005
} ;
1004
1006
1005
1007
let features = {
@@ -1076,7 +1078,62 @@ pub enum Amount {
1076
1078
}
1077
1079
1078
1080
/// 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
+ }
1080
1137
1081
1138
/// Quantity of items supported by an [`Offer`].
1082
1139
#[ derive( Clone , Copy , Debug , PartialEq ) ]
@@ -1115,7 +1172,7 @@ const OFFER_ISSUER_ID_TYPE: u64 = 22;
1115
1172
tlv_stream ! ( OfferTlvStream , OfferTlvStreamRef <' a>, OFFER_TYPES , {
1116
1173
( 2 , chains: ( Vec <ChainHash >, WithoutLength ) ) ,
1117
1174
( OFFER_METADATA_TYPE , metadata: ( Vec <u8 >, WithoutLength ) ) ,
1118
- ( 6 , currency: CurrencyCode ) ,
1175
+ ( 6 , currency: [ u8 ; 3 ] ) ,
1119
1176
( 8 , amount: ( u64 , HighZeroBytesDroppedBigSize ) ) ,
1120
1177
( 10 , description: ( String , WithoutLength ) ) ,
1121
1178
( 12 , features: ( OfferFeatures , WithoutLength ) ) ,
@@ -1209,7 +1266,11 @@ impl TryFrom<FullOfferTlvStream> for OfferContents {
1209
1266
} ,
1210
1267
( None , Some ( amount_msats) ) => Some ( Amount :: Bitcoin { amount_msats } ) ,
1211
1268
( 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
+ } ,
1213
1274
} ;
1214
1275
1215
1276
if amount. is_some ( ) && description. is_none ( ) {
@@ -1256,6 +1317,37 @@ impl core::fmt::Display for Offer {
1256
1317
}
1257
1318
}
1258
1319
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 {
1325
+ /// The actual length of the currency code
1326
+ actual : usize
1327
+ } ,
1328
+ /// The currency code contains invalid UTF-8
1329
+ InvalidUtf8 ,
1330
+ /// The currency code must be all ASCII uppercase letters
1331
+ NotAsciiUppercase {
1332
+ /// The actual currency code
1333
+ code : String
1334
+ } ,
1335
+ }
1336
+
1337
+ impl core:: fmt:: Display for CurrencyCodeError {
1338
+ fn fmt ( & self , f : & mut core:: fmt:: Formatter < ' _ > ) -> core:: fmt:: Result {
1339
+ match self {
1340
+ Self :: InvalidLength { actual } => {
1341
+ write ! ( f, "Currency code must be 3 bytes, got {}" , actual)
1342
+ } ,
1343
+ Self :: InvalidUtf8 => write ! ( f, "Currency code contains invalid UTF-8" ) ,
1344
+ Self :: NotAsciiUppercase { code } => {
1345
+ write ! ( f, "Currency code '{}' must be all ASCII uppercase" , code)
1346
+ } ,
1347
+ }
1348
+ }
1349
+ }
1350
+
1259
1351
#[ cfg( test) ]
1260
1352
mod tests {
1261
1353
#[ cfg( not( c_bindings) ) ]
@@ -1273,6 +1365,7 @@ mod tests {
1273
1365
use crate :: ln:: inbound_payment:: ExpandedKey ;
1274
1366
use crate :: ln:: msgs:: { DecodeError , MAX_VALUE_MSAT } ;
1275
1367
use crate :: offers:: nonce:: Nonce ;
1368
+ use crate :: offers:: offer:: CurrencyCode ;
1276
1369
use crate :: offers:: parse:: { Bolt12ParseError , Bolt12SemanticError } ;
1277
1370
use crate :: offers:: test_utils:: * ;
1278
1371
use crate :: types:: features:: OfferFeatures ;
@@ -1541,7 +1634,8 @@ mod tests {
1541
1634
#[ test]
1542
1635
fn builds_offer_with_amount ( ) {
1543
1636
let bitcoin_amount = Amount :: Bitcoin { amount_msats : 1000 } ;
1544
- let currency_amount = Amount :: Currency { iso4217_code : * b"USD" , amount : 10 } ;
1637
+ let currency_amount =
1638
+ Amount :: Currency { iso4217_code : CurrencyCode :: new ( * b"USD" ) . unwrap ( ) , amount : 10 } ;
1545
1639
1546
1640
let offer = OfferBuilder :: new ( pubkey ( 42 ) ) . amount_msats ( 1000 ) . build ( ) . unwrap ( ) ;
1547
1641
let tlv_stream = offer. as_tlv_stream ( ) ;
@@ -1820,6 +1914,36 @@ mod tests {
1820
1914
Bolt12ParseError :: InvalidSemantics ( Bolt12SemanticError :: InvalidAmount )
1821
1915
) ,
1822
1916
}
1917
+
1918
+ let mut tlv_stream = offer. as_tlv_stream ( ) ;
1919
+ tlv_stream. 0 . amount = Some ( 1000 ) ;
1920
+ tlv_stream. 0 . currency = Some ( b"\xFF \xFE \xFD " ) ; // invalid UTF-8 bytes
1921
+
1922
+ let mut encoded_offer = Vec :: new ( ) ;
1923
+ tlv_stream. write ( & mut encoded_offer) . unwrap ( ) ;
1924
+
1925
+ match Offer :: try_from ( encoded_offer) {
1926
+ Ok ( _) => panic ! ( "expected error" ) ,
1927
+ Err ( e) => assert_eq ! (
1928
+ e,
1929
+ Bolt12ParseError :: InvalidSemantics ( Bolt12SemanticError :: InvalidCurrencyCode )
1930
+ ) ,
1931
+ }
1932
+
1933
+ let mut tlv_stream = offer. as_tlv_stream ( ) ;
1934
+ tlv_stream. 0 . amount = Some ( 1000 ) ;
1935
+ tlv_stream. 0 . currency = Some ( b"usd" ) ; // invalid ISO 4217 code
1936
+
1937
+ let mut encoded_offer = Vec :: new ( ) ;
1938
+ tlv_stream. write ( & mut encoded_offer) . unwrap ( ) ;
1939
+
1940
+ match Offer :: try_from ( encoded_offer) {
1941
+ Ok ( _) => panic ! ( "expected error" ) ,
1942
+ Err ( e) => assert_eq ! (
1943
+ e,
1944
+ Bolt12ParseError :: InvalidSemantics ( Bolt12SemanticError :: InvalidCurrencyCode )
1945
+ ) ,
1946
+ }
1823
1947
}
1824
1948
1825
1949
#[ test]
0 commit comments