@@ -86,3 +86,149 @@ struct Recurrence {
86
86
}
87
87
88
88
impl_writeable ! ( Recurrence , { time_unit, period } ) ;
89
+
90
+ /// Error when parsing a bech32 encoded message using [`str::parse`].
91
+ #[ derive( Debug , PartialEq ) ]
92
+ pub enum ParseError {
93
+ /// The bech32 encoding does not conform to the BOLT 12 requirements for continuing messages
94
+ /// across multiple parts (i.e., '+' followed by whitespace).
95
+ InvalidContinuation ,
96
+ /// The bech32 encoding's human-readable part does not match what was expected for the message
97
+ /// being parsed.
98
+ InvalidBech32Hrp ,
99
+ /// The string could not be bech32 decoded.
100
+ Bech32 ( bech32:: Error ) ,
101
+ /// The bech32 decoded string could not be decoded as the expected message type.
102
+ Decode ( DecodeError ) ,
103
+ }
104
+
105
+ impl From < bech32:: Error > for ParseError {
106
+ fn from ( error : bech32:: Error ) -> Self {
107
+ Self :: Bech32 ( error)
108
+ }
109
+ }
110
+
111
+ impl From < DecodeError > for ParseError {
112
+ fn from ( error : DecodeError ) -> Self {
113
+ Self :: Decode ( error)
114
+ }
115
+ }
116
+
117
+ const OFFER_BECH32_HRP : & str = "lno" ;
118
+
119
+ impl FromStr for OfferTlvStream {
120
+ type Err = ParseError ;
121
+
122
+ fn from_str ( s : & str ) -> Result < Self , <Self as FromStr >:: Err > {
123
+ // Offer encoding may be split by '+' followed by optional whitespace.
124
+ for chunk in s. split ( '+' ) {
125
+ let chunk = chunk. trim_start ( ) ;
126
+ if chunk. is_empty ( ) || chunk. contains ( char:: is_whitespace) {
127
+ return Err ( ParseError :: InvalidContinuation ) ;
128
+ }
129
+ }
130
+
131
+ let s = s. chars ( ) . filter ( |c| * c != '+' && !c. is_whitespace ( ) ) . collect :: < String > ( ) ;
132
+ let ( hrp, data) = bech32:: decode_without_checksum ( & s) ?;
133
+
134
+ if hrp != OFFER_BECH32_HRP {
135
+ return Err ( ParseError :: InvalidBech32Hrp ) ;
136
+ }
137
+
138
+ let data = Vec :: < u8 > :: from_base32 ( & data) ?;
139
+ Ok ( Readable :: read ( & mut & data[ ..] ) ?)
140
+ }
141
+ }
142
+
143
+ impl core:: fmt:: Display for OfferTlvStream {
144
+ fn fmt ( & self , f : & mut core:: fmt:: Formatter ) -> Result < ( ) , core:: fmt:: Error > {
145
+ use util:: ser:: Writeable ;
146
+ let mut buffer = Vec :: new ( ) ;
147
+ self . write ( & mut buffer) . unwrap ( ) ;
148
+
149
+ use bitcoin:: bech32:: ToBase32 ;
150
+ let data = buffer. to_base32 ( ) ;
151
+ bech32:: encode_without_checksum_to_fmt ( f, OFFER_BECH32_HRP , data) . expect ( "HRP is valid" ) . unwrap ( ) ;
152
+
153
+ Ok ( ( ) )
154
+ }
155
+ }
156
+
157
+ #[ cfg( test) ]
158
+ mod tests {
159
+ use super :: { OfferTlvStream , ParseError } ;
160
+ use bitcoin:: bech32;
161
+ use ln:: msgs:: DecodeError ;
162
+
163
+ #[ test]
164
+ fn encodes_offer_as_bech32_without_checksum ( ) {
165
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ;
166
+ let offer = encoded_offer. parse :: < OfferTlvStream > ( ) . unwrap ( ) ;
167
+ assert_eq ! ( offer. to_string( ) , encoded_offer) ;
168
+ }
169
+
170
+ #[ test]
171
+ fn parses_bech32_encoded_offers ( ) {
172
+ let offers = [
173
+ // BOLT 12 test vectors
174
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
175
+ "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
176
+ "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
177
+ "lno1qcp4256ypqpq+86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qs+y" ,
178
+ "lno1qcp4256ypqpq+ 86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+ 0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+\n sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43l+\r \n astpwuh73k29qs+\r y" ,
179
+ // Two blinded paths
180
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0yg06qg2qdd7t628sgykwj5kuc837qmlv9m9gr7sq8ap6erfgacv26nhp8zzcqgzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6heqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9sqs9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
181
+ ] ;
182
+ for encoded_offer in & offers {
183
+ if let Err ( e) = encoded_offer. parse :: < OfferTlvStream > ( ) {
184
+ panic ! ( "Invalid offer ({:?}): {}" , e, encoded_offer) ;
185
+ }
186
+ }
187
+ }
188
+
189
+ #[ test]
190
+ fn fails_parsing_bech32_encoded_offers_with_invalid_continuations ( ) {
191
+ let offers = [
192
+ // BOLT 12 test vectors
193
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+" ,
194
+ "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+ " ,
195
+ "+lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
196
+ "+ lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
197
+ "ln++o1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ,
198
+ ] ;
199
+ for encoded_offer in & offers {
200
+ match encoded_offer. parse :: < OfferTlvStream > ( ) {
201
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
202
+ Err ( e) => assert_eq ! ( e, ParseError :: InvalidContinuation ) ,
203
+ }
204
+ }
205
+
206
+ }
207
+
208
+ #[ test]
209
+ fn fails_parsing_bech32_encoded_offer_with_invalid_hrp ( ) {
210
+ let encoded_offer = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy" ;
211
+ match encoded_offer. parse :: < OfferTlvStream > ( ) {
212
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
213
+ Err ( e) => assert_eq ! ( e, ParseError :: InvalidBech32Hrp ) ,
214
+ }
215
+ }
216
+
217
+ #[ test]
218
+ fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data ( ) {
219
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso" ;
220
+ match encoded_offer. parse :: < OfferTlvStream > ( ) {
221
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
222
+ Err ( e) => assert_eq ! ( e, ParseError :: Bech32 ( bech32:: Error :: InvalidChar ( 'o' ) ) ) ,
223
+ }
224
+ }
225
+
226
+ #[ test]
227
+ fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data ( ) {
228
+ let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq" ;
229
+ match encoded_offer. parse :: < OfferTlvStream > ( ) {
230
+ Ok ( _) => panic ! ( "Valid offer: {}" , encoded_offer) ,
231
+ Err ( e) => assert_eq ! ( e, ParseError :: Decode ( DecodeError :: InvalidValue ) ) ,
232
+ }
233
+ }
234
+ }
0 commit comments