@@ -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,23 @@ 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 for robustness across different wallet implementations.
213+ /// Uses `ton_contracts::jetton::ForwardPayload` to extract the memo from `forward_payload`,
214+ /// handling both inline (Either bit=0) and ref (Either bit=1) cases.
211215fn parse_jetton_transfer_body (
212216 parser : & mut tlb_ton:: de:: CellParser < ' _ > ,
213- ) -> Result < JettonTransferFields , WasmTonError > {
217+ body : & Cell ,
218+ ) -> Result < ( JettonTransferFields , Option < String > ) , WasmTonError > {
214219 // query_id: uint64
215220 let query_id: u64 = parser
216221 . unpack ( ( ) )
@@ -238,10 +243,9 @@ fn parse_jetton_transfer_body(
238243 response_dst. to_base64_url_flags ( false , false )
239244 } ;
240245
241- // custom_payload: Maybe ^Cell ( skip it)
246+ // custom_payload: Maybe ^Cell — skip if present
242247 let has_custom_payload: bool = parser. unpack ( ( ) ) . unwrap_or ( false ) ;
243248 if has_custom_payload {
244- // Skip the ref
245249 let _: Cell = parser. parse_as :: < _ , tlb_ton:: Ref > ( ( ) ) . unwrap_or_default ( ) ;
246250 }
247251
@@ -251,14 +255,67 @@ fn parse_jetton_transfer_body(
251255 } ) ?;
252256 let forward_ton_amount = biguint_to_u64 ( & forward_big) ;
253257
254- Ok ( JettonTransferFields {
255- query_id,
256- amount,
257- destination,
258- response_destination,
259- forward_ton_amount,
260- forward_payload : None ,
261- } )
258+ // forward_payload: Either Cell ^Cell — extract text memo if present
259+ let memo = parse_forward_payload_memo ( parser, body) ;
260+
261+ Ok ( (
262+ JettonTransferFields {
263+ query_id,
264+ amount,
265+ destination,
266+ response_destination,
267+ forward_ton_amount,
268+ } ,
269+ memo,
270+ ) )
271+ }
272+
273+ /// Extract a text memo from `forward_payload:(Either Cell ^Cell)`.
274+ ///
275+ /// Handles both inline (bit=0) and ref (bit=1) forward_payload storage.
276+ /// Returns `None` on any parse error or if the payload is not a text comment.
277+ fn parse_forward_payload_memo (
278+ parser : & mut tlb_ton:: de:: CellParser < ' _ > ,
279+ _body : & Cell ,
280+ ) -> Option < String > {
281+ // Read the Either bit: 0 = inline, 1 = ref
282+ let is_ref: bool = parser. unpack ( ( ) ) . ok ( ) ?;
283+
284+ if is_ref {
285+ // Payload is in the next ref cell — read the ref and parse from there
286+ let ref_cell: Cell = parser. parse_as :: < _ , tlb_ton:: Ref > ( ( ) ) . ok ( ) ?;
287+ read_text_comment ( & mut ref_cell. parser ( ) )
288+ } else {
289+ // Payload is inline in the remaining bits
290+ read_text_comment ( parser)
291+ }
292+ }
293+
294+ /// Read a TEP-74 text comment: `0x00000000` prefix followed by UTF-8 bytes.
295+ /// Returns `None` if the data doesn't start with the comment prefix or is not valid text.
296+ fn read_text_comment ( parser : & mut tlb_ton:: de:: CellParser < ' _ > ) -> Option < String > {
297+ const COMMENT_PREFIX : u32 = 0x0000_0000 ;
298+ if parser. bits_left ( ) < 32 {
299+ return None ;
300+ }
301+ let prefix: u32 = parser. unpack ( ( ) ) . ok ( ) ?;
302+ if prefix != COMMENT_PREFIX {
303+ return None ;
304+ }
305+ let remaining = parser. bits_left ( ) / 8 ;
306+ let mut bytes = Vec :: with_capacity ( remaining) ;
307+ for _ in 0 ..remaining {
308+ match parser. unpack :: < u8 > ( ( ) ) {
309+ Ok ( b) => bytes. push ( b) ,
310+ Err ( _) => break ,
311+ }
312+ }
313+ let text = String :: from_utf8_lossy ( & bytes) . into_owned ( ) ;
314+ if text. is_empty ( ) {
315+ None
316+ } else {
317+ Some ( text)
318+ }
262319}
263320
264321fn determine_transaction_type ( actions : & [ ParsedSendAction ] ) -> TransactionType {
@@ -313,3 +370,30 @@ fn biguint_to_u64(v: &BigUint) -> u64 {
313370 v. to_u64_digits ( ) . first ( ) . copied ( ) . unwrap_or ( 0 )
314371 }
315372}
373+
374+ #[ cfg( test) ]
375+ mod parser_tests {
376+ use super :: * ;
377+ use crate :: transaction:: Transaction ;
378+ use base64:: { engine:: general_purpose:: STANDARD , Engine } ;
379+
380+ /// signedTokenSendTransaction.tx from sdk-coin-ton fixtures.
381+ /// forward_payload is stored as a ref cell (Either bit=1) with memo "jetton testing".
382+ 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==" ;
383+
384+ #[ test]
385+ fn test_jetton_transfer_memo_from_ref_cell ( ) {
386+ // Verifies that memos stored as ref cells (forward_payload bit=1) are correctly extracted.
387+ let bytes = STANDARD . decode ( TOKEN_TX ) . unwrap ( ) ;
388+ let tx = Transaction :: from_bytes ( & bytes) . unwrap ( ) ;
389+ let parsed = parse_from_transaction ( & tx) . unwrap ( ) ;
390+ assert_eq ! ( parsed. transaction_type, TransactionType :: TokenTransfer ) ;
391+ let action = & parsed. send_actions [ 0 ] ;
392+ assert ! ( action. jetton_transfer. is_some( ) ) ;
393+ assert_eq ! ( action. memo. as_deref( ) , Some ( "jetton testing" ) ) ;
394+ assert_eq ! (
395+ action. jetton_transfer. as_ref( ) . unwrap( ) . amount,
396+ 1_000_000_000
397+ ) ;
398+ }
399+ }
0 commit comments