@@ -42,15 +42,14 @@ const WHALES_DEPOSIT_OPCODE: u32 = 0x7bcd1fef;
4242const WHALES_WITHDRAW_OPCODE : u32 = 0xda803efd ;
4343const SINGLE_NOMINATOR_WITHDRAW_OPCODE : u32 = 0x00001000 ; // 4096
4444
45- /// Parsed jetton transfer fields (manually parsed, not using ton_contracts JettonTransfer)
45+ /// Parsed jetton transfer fields exposed to the JS layer.
4646#[ derive( Debug , Clone ) ]
4747pub struct JettonTransferFields {
4848 pub query_id : u64 ,
4949 pub amount : u64 ,
5050 pub destination : String ,
5151 pub response_destination : String ,
5252 pub forward_ton_amount : u64 ,
53- pub forward_payload : Option < Vec < u8 > > ,
5453}
5554
5655/// A single send action parsed from the transaction
@@ -200,17 +199,24 @@ fn parse_message_body(body: &Cell) -> Result<BodyParseResult, WasmTonError> {
200199 }
201200
202201 if opcode == JETTON_TRANSFER_OPCODE {
203- let jetton = parse_jetton_transfer_body ( & mut parser) ?;
204- return Ok ( ( Some ( opcode) , None , Some ( jetton) ) ) ;
202+ let ( jetton, memo ) = parse_jetton_transfer_body ( & mut parser, body ) ?;
203+ return Ok ( ( Some ( opcode) , memo , Some ( jetton) ) ) ;
205204 }
206205
207206 // Other known opcodes
208207 Ok ( ( Some ( opcode) , None , None ) )
209208}
210209
210+ /// Parse a jetton transfer body, returning the parsed fields and any text memo.
211+ ///
212+ /// Parses TEP-74 fields manually instead of using `JettonTransfer::<Cell>::parse` due to
213+ /// a bug in `tlbits` 0.7.3: `Remainder::unpack_as` for byte/string types passes `bits_left()`
214+ /// (bits) to `BorrowCow` which expects bytes, causing "EOF" errors when parsing text comments.
215+ /// See `test_tlbits_remainder_bug_prevents_crate_jetton_parse` for the proof.
211216fn parse_jetton_transfer_body (
212217 parser : & mut tlb_ton:: de:: CellParser < ' _ > ,
213- ) -> Result < JettonTransferFields , WasmTonError > {
218+ body : & Cell ,
219+ ) -> Result < ( JettonTransferFields , Option < String > ) , WasmTonError > {
214220 // query_id: uint64
215221 let query_id: u64 = parser
216222 . unpack ( ( ) )
@@ -238,10 +244,9 @@ fn parse_jetton_transfer_body(
238244 response_dst. to_base64_url_flags ( false , false )
239245 } ;
240246
241- // custom_payload: Maybe ^Cell ( skip it)
247+ // custom_payload: Maybe ^Cell — skip if present
242248 let has_custom_payload: bool = parser. unpack ( ( ) ) . unwrap_or ( false ) ;
243249 if has_custom_payload {
244- // Skip the ref
245250 let _: Cell = parser. parse_as :: < _ , tlb_ton:: Ref > ( ( ) ) . unwrap_or_default ( ) ;
246251 }
247252
@@ -251,14 +256,67 @@ fn parse_jetton_transfer_body(
251256 } ) ?;
252257 let forward_ton_amount = biguint_to_u64 ( & forward_big) ;
253258
254- Ok ( JettonTransferFields {
255- query_id,
256- amount,
257- destination,
258- response_destination,
259- forward_ton_amount,
260- forward_payload : None ,
261- } )
259+ // forward_payload: Either Cell ^Cell — extract text memo if present
260+ let memo = parse_forward_payload_memo ( parser, body) ;
261+
262+ Ok ( (
263+ JettonTransferFields {
264+ query_id,
265+ amount,
266+ destination,
267+ response_destination,
268+ forward_ton_amount,
269+ } ,
270+ memo,
271+ ) )
272+ }
273+
274+ /// Extract a text memo from `forward_payload:(Either Cell ^Cell)`.
275+ ///
276+ /// Handles both inline (bit=0) and ref (bit=1) forward_payload storage.
277+ /// Returns `None` on any parse error or if the payload is not a text comment.
278+ fn parse_forward_payload_memo (
279+ parser : & mut tlb_ton:: de:: CellParser < ' _ > ,
280+ _body : & Cell ,
281+ ) -> Option < String > {
282+ // Read the Either bit: 0 = inline, 1 = ref
283+ let is_ref: bool = parser. unpack ( ( ) ) . ok ( ) ?;
284+
285+ if is_ref {
286+ // Payload is in the next ref cell — read the ref and parse from there
287+ let ref_cell: Cell = parser. parse_as :: < _ , tlb_ton:: Ref > ( ( ) ) . ok ( ) ?;
288+ read_text_comment ( & mut ref_cell. parser ( ) )
289+ } else {
290+ // Payload is inline in the remaining bits
291+ read_text_comment ( parser)
292+ }
293+ }
294+
295+ /// Read a TEP-74 text comment: `0x00000000` prefix followed by UTF-8 bytes.
296+ /// Returns `None` if the data doesn't start with the comment prefix or is not valid text.
297+ fn read_text_comment ( parser : & mut tlb_ton:: de:: CellParser < ' _ > ) -> Option < String > {
298+ const COMMENT_PREFIX : u32 = 0x0000_0000 ;
299+ if parser. bits_left ( ) < 32 {
300+ return None ;
301+ }
302+ let prefix: u32 = parser. unpack ( ( ) ) . ok ( ) ?;
303+ if prefix != COMMENT_PREFIX {
304+ return None ;
305+ }
306+ let remaining = parser. bits_left ( ) / 8 ;
307+ let mut bytes = Vec :: with_capacity ( remaining) ;
308+ for _ in 0 ..remaining {
309+ match parser. unpack :: < u8 > ( ( ) ) {
310+ Ok ( b) => bytes. push ( b) ,
311+ Err ( _) => break ,
312+ }
313+ }
314+ let text = String :: from_utf8_lossy ( & bytes) . into_owned ( ) ;
315+ if text. is_empty ( ) {
316+ None
317+ } else {
318+ Some ( text)
319+ }
262320}
263321
264322fn determine_transaction_type ( actions : & [ ParsedSendAction ] ) -> TransactionType {
@@ -313,3 +371,86 @@ fn biguint_to_u64(v: &BigUint) -> u64 {
313371 v. to_u64_digits ( ) . first ( ) . copied ( ) . unwrap_or ( 0 )
314372 }
315373}
374+
375+ #[ cfg( test) ]
376+ mod parser_tests {
377+ use super :: * ;
378+ use crate :: transaction:: Transaction ;
379+ use base64:: { engine:: general_purpose:: STANDARD , Engine } ;
380+ use tlb_ton:: de:: CellDeserialize ;
381+ use tlb_ton:: Cell ;
382+ use ton_contracts:: jetton:: JettonTransfer ;
383+ use ton_contracts:: wallet:: v4r2:: WalletV4R2Op ;
384+
385+ /// signedTokenSendTransaction.tx from sdk-coin-ton fixtures.
386+ /// forward_payload is stored as a ref cell (Either bit=1) with memo "jetton testing".
387+ const TOKEN_TX : & str = "te6cckECGgEABB0AAuGIAVSGb+UGjjP3lvt+zFA8wouI3McEd6CKbO2TwcZ3OfLKGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACmpoxdJlgLSAAAAAAADgEXAgE0AhYBFP8A9KQT9LzyyAsDAgEgBBECAUgFCALm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQYHAHgB+gD0BDD4J28iMFAKoSG+8uBQghBwbHVngx6xcIAYUATLBSbPFlj6Ahn0AMtpF8sfUmDLPyDJgED7AAYAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gIBIAkQAgEgCg8CAVgLDAA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIA0OABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AABG4yX7UTQ1wsfgAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/xITFBUAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJc/sAyEAUgQEI9FHypwIAcIEBCNcY+gDTP8hUIEeBAQj0UfKnghBub3RlcHSAGMjLBcsCUAbPFlAE+gIUy2oSyx/LP8lz+wACAGyBAQjXGPoA0z8wUiSBAQj0WfKnghBkc3RycHSAGMjLBcsCUAXPFlAD+gITy2rLHxLLP8lz+wAACvQAye1UAFEAAAAAKamjF9NTAQHUHhbX00VGZ3d2r8hbJxuz7PaxmuCOJ6kgckppQAFmQgABT9LR3Iqffskp0J9gWYO8Azlnb33BCMj8FqIUIGxGOZpiWgAAAAAAAAAAAAAAAAABGAGuD4p+pQAAAAAAAAAAQ7msoAgA/BGdBi/R01erquxJOvPgGKclBawUs3MAi0/IdctKQz8AKpDN/KDRxn7y32/ZigeYUXEbmOCO9BFNnbJ4OM7nPllGHoSBGQAkAAAAAGpldHRvbiB0ZXN0aW5nwHtw7A==" ;
388+
389+ /// Demonstrates a bug in `tlbits` 0.7.3 `Remainder` adapter that prevents
390+ /// `JettonTransfer::<Cell>::parse` from working on messages with text comments.
391+ ///
392+ /// Root cause: `Remainder::unpack_as` for byte-oriented types (`Cow<[u8]>`,
393+ /// `Vec<u8>`, `Cow<str>`, `String`) passes `bits_left()` (a value in bits) to
394+ /// `BorrowCow` which expects the argument in bytes. This causes `BorrowCow` to
395+ /// attempt reading `bits_left * 8` bits, resulting in an "EOF" error.
396+ ///
397+ /// File: `tlbits-0.7.3/src/as/remainder.rs` lines 40-51 and 65-76.
398+ /// Upstream: https://github.com/mitinarseny/toner
399+ ///
400+ /// Until this is fixed upstream, we parse jetton transfer fields manually in
401+ /// `parse_jetton_transfer_body` / `parse_forward_payload_memo`.
402+ #[ test]
403+ fn test_tlbits_remainder_bug_prevents_crate_jetton_parse ( ) {
404+ let bytes = STANDARD . decode ( TOKEN_TX ) . unwrap ( ) ;
405+ let tx = Transaction :: from_bytes ( & bytes) . unwrap ( ) ;
406+ let sign_body = tx. sign_body ( ) ;
407+
408+ let actions = match & sign_body. op {
409+ WalletV4R2Op :: Send ( actions) => actions,
410+ _ => panic ! ( "expected Send op" ) ,
411+ } ;
412+ let body = & actions[ 0 ] . message . body ;
413+ let ref_cell = & * body. references [ 0 ] ;
414+
415+ // BorrowCow with correct byte count works
416+ use tlb_ton:: bits:: de:: BitReaderExt ;
417+ let mut p1 = ref_cell. parser ( ) ;
418+ let _: u32 = p1. unpack ( ( ) ) . unwrap ( ) ; // skip 0x00000000 comment prefix
419+ let bytes_left = p1. bits_left ( ) / 8 ;
420+ let ok: Result < std:: borrow:: Cow < str > , _ > =
421+ p1. unpack_as :: < _ , tlb_ton:: bits:: BorrowCow > ( bytes_left) ;
422+ assert_eq ! ( ok. unwrap( ) . as_ref( ) , "jetton testing" ) ;
423+
424+ // Remainder passes bits_left() (112) to BorrowCow which expects bytes (14)
425+ let mut p2 = ref_cell. parser ( ) ;
426+ let _: u32 = p2. unpack ( ( ) ) . unwrap ( ) ;
427+ let err = p2
428+ . unpack_as :: < String , tlb_ton:: bits:: Remainder > ( ( ) )
429+ . unwrap_err ( ) ;
430+ assert_eq ! ( err. to_string( ) , "EOF" ) ;
431+
432+ // JettonTransfer::<Cell>::parse fails due to the same Remainder bug
433+ let mut parser = body. parser ( ) ;
434+ let err = JettonTransfer :: < Cell > :: parse ( & mut parser, ( ) ) . unwrap_err ( ) ;
435+ assert ! (
436+ err. to_string( ) . contains( "EOF" ) ,
437+ "expected EOF error, got: {err}"
438+ ) ;
439+ }
440+
441+ #[ test]
442+ fn test_jetton_transfer_memo_from_ref_cell ( ) {
443+ // Verifies that memos stored as ref cells (forward_payload bit=1) are correctly extracted.
444+ let bytes = STANDARD . decode ( TOKEN_TX ) . unwrap ( ) ;
445+ let tx = Transaction :: from_bytes ( & bytes) . unwrap ( ) ;
446+ let parsed = parse_from_transaction ( & tx) . unwrap ( ) ;
447+ assert_eq ! ( parsed. transaction_type, TransactionType :: TokenTransfer ) ;
448+ let action = & parsed. send_actions [ 0 ] ;
449+ assert ! ( action. jetton_transfer. is_some( ) ) ;
450+ assert_eq ! ( action. memo. as_deref( ) , Some ( "jetton testing" ) ) ;
451+ assert_eq ! (
452+ action. jetton_transfer. as_ref( ) . unwrap( ) . amount,
453+ 1_000_000_000
454+ ) ;
455+ }
456+ }
0 commit comments