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