33//! Provides a single `parse_transaction` function that deserializes transaction bytes
44//! and decodes all instructions into semantic types matching BitGoJS's TxData format.
55//!
6- //! The parser performs post-processing to combine sequential instructions into
7- //! compound types that match BitGoJS's semantic representation:
8- //! - CreateAccount + NonceInitialize → CreateNonceAccount
9- //! - CreateAccount + StakeInitialize + StakingDelegate → StakingActivate
10-
11- use crate :: instructions:: {
12- decode_instruction, CreateNonceAccountParams , InstructionContext , ParsedInstruction ,
13- StakingActivateParams , STAKE_PROGRAM_ID , SYSTEM_PROGRAM_ID ,
14- } ;
6+ //! This parser returns raw decoded instructions. Instruction combining (e.g.,
7+ //! CreateAccount + NonceInitialize → CreateNonceAccount) is handled by the
8+ //! TypeScript consumer (mapWasmInstructionsToBitGoJS in BitGoJS).
9+
10+ use crate :: instructions:: { decode_instruction, InstructionContext , ParsedInstruction } ;
1511use crate :: transaction:: { Transaction , TransactionExt } ;
1612use serde:: Serialize ;
1713
@@ -126,8 +122,8 @@ pub fn parse_transaction(bytes: &[u8]) -> Result<ParsedTransaction, String> {
126122 instructions_data. push ( parsed) ;
127123 }
128124
129- // Post-process: combine sequential instructions into compound types
130- let instructions_data = combine_instructions ( instructions_data ) ;
125+ // Note: Instruction combining (e.g., CreateAccount + StakeInitialize → StakingActivate)
126+ // is handled by TypeScript in mapWasmInstructionsToBitGoJS for flexibility
131127
132128 // The nonce is either the blockhash or, for durable nonce txs, still the blockhash
133129 // (which is the nonce value from the nonce account)
@@ -144,109 +140,6 @@ pub fn parse_transaction(bytes: &[u8]) -> Result<ParsedTransaction, String> {
144140 } )
145141}
146142
147- /// Combine sequential instructions into compound semantic types.
148- ///
149- /// This matches BitGoJS's behavior where certain instruction sequences are
150- /// represented as a single high-level instruction:
151- /// - CreateAccount + NonceInitialize → CreateNonceAccount
152- /// - CreateAccount + StakeInitialize + StakingDelegate → StakingActivate
153- fn combine_instructions ( instructions : Vec < ParsedInstruction > ) -> Vec < ParsedInstruction > {
154- let mut result = Vec :: with_capacity ( instructions. len ( ) ) ;
155- let mut i = 0 ;
156-
157- while i < instructions. len ( ) {
158- // Try to match CreateAccount patterns
159- if let ParsedInstruction :: CreateAccount ( ref create) = instructions[ i] {
160- // Pattern 1: CreateAccount + NonceInitialize → CreateNonceAccount
161- if i + 1 < instructions. len ( ) {
162- if let ParsedInstruction :: NonceInitialize ( ref nonce_init) = instructions[ i + 1 ] {
163- // Check if CreateAccount target matches NonceInitialize nonce address
164- // and owner is System Program (nonce accounts owned by system program)
165- if create. new_address == nonce_init. nonce_address
166- && create. owner == SYSTEM_PROGRAM_ID
167- {
168- result. push ( ParsedInstruction :: CreateNonceAccount (
169- CreateNonceAccountParams {
170- from_address : create. from_address . clone ( ) ,
171- nonce_address : nonce_init. nonce_address . clone ( ) ,
172- auth_address : nonce_init. auth_address . clone ( ) ,
173- amount : create. amount . clone ( ) ,
174- } ,
175- ) ) ;
176- i += 2 ; // Skip both instructions
177- continue ;
178- }
179- }
180- }
181-
182- // Pattern 2: CreateAccount + StakeInitialize + StakingDelegate → StakingActivate (NATIVE)
183- // Must check this 3-instruction pattern BEFORE the 2-instruction Marinade pattern
184- if i + 2 < instructions. len ( ) {
185- if let (
186- ParsedInstruction :: StakeInitialize ( ref stake_init) ,
187- ParsedInstruction :: StakingDelegate ( ref delegate) ,
188- ) = ( & instructions[ i + 1 ] , & instructions[ i + 2 ] )
189- {
190- // Check if CreateAccount target matches StakeInitialize staking address
191- // and owner is Stake Program
192- if create. new_address == stake_init. staking_address
193- && create. owner == STAKE_PROGRAM_ID
194- && stake_init. staking_address == delegate. staking_address
195- {
196- result. push ( ParsedInstruction :: StakingActivate ( StakingActivateParams {
197- from_address : create. from_address . clone ( ) ,
198- staking_address : stake_init. staking_address . clone ( ) ,
199- amount : create. amount . clone ( ) ,
200- validator : delegate. validator . clone ( ) ,
201- staking_type : "NATIVE" . to_string ( ) ,
202- } ) ) ;
203- i += 3 ; // Skip all three instructions
204- continue ;
205- }
206- }
207- }
208-
209- // Pattern 3: CreateAccount + StakeInitialize (without Delegate) → StakingActivate (MARINADE)
210- // Marinade staking creates a stake account but doesn't delegate to a validator
211- // The "validator" field stores the authorized staker address instead
212- if i + 1 < instructions. len ( ) {
213- if let ParsedInstruction :: StakeInitialize ( ref stake_init) = instructions[ i + 1 ] {
214- // Check if CreateAccount target matches StakeInitialize staking address
215- // and owner is Stake Program
216- // Also make sure the next instruction (if any) is NOT a StakingDelegate
217- let is_not_followed_by_delegate = i + 2 >= instructions. len ( )
218- || !matches ! (
219- instructions[ i + 2 ] ,
220- ParsedInstruction :: StakingDelegate ( _)
221- ) ;
222-
223- if create. new_address == stake_init. staking_address
224- && create. owner == STAKE_PROGRAM_ID
225- && is_not_followed_by_delegate
226- {
227- result. push ( ParsedInstruction :: StakingActivate ( StakingActivateParams {
228- from_address : create. from_address . clone ( ) ,
229- staking_address : stake_init. staking_address . clone ( ) ,
230- amount : create. amount . clone ( ) ,
231- // For Marinade, the validator field stores the authorized staker
232- validator : stake_init. staker . clone ( ) ,
233- staking_type : "MARINADE" . to_string ( ) ,
234- } ) ) ;
235- i += 2 ; // Skip both instructions
236- continue ;
237- }
238- }
239- }
240- }
241-
242- // No pattern matched, keep the instruction as-is
243- result. push ( instructions[ i] . clone ( ) ) ;
244- i += 1 ;
245- }
246-
247- result
248- }
249-
250143/// Serialize signatures as base64 strings for JSON output.
251144mod signatures_serde {
252145 use base64:: prelude:: * ;
@@ -312,6 +205,7 @@ mod tests {
312205 }
313206
314207 // Marinade staking activate transaction (CreateAccount + StakeInitialize without Delegate)
208+ // Note: Combining is now done in TypeScript, so we expect raw instructions here
315209 const MARINADE_STAKING_ACTIVATE : & str = "AuRFS0r7hJ+/+WuDQbbwdjSgxfnKOWi94EnWEha9uaBPt8VZOXiOoSiSoES34VkyBNLlLqlfK0fP3d5eJR+srQvN04gqzpOZPTVzqiomyMXqwQ6FYoQg5nEkdiDVny8SsyhRnAeDMzexkKD+3rwSGP0E+XN/2crTL6PZRnip42YFAgADBUXlebz5JTz2i0ff8fs6OlwsIbrFsjwJrhKm4FVr8ItBYnsvugEnYfm5Gbz5TLtMncgFHZ8JMpkxTTlJIzJovekAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAah2BeRN1QqmDQ3vf4qerJVf1NcinhyK2ikncAAAAAABqfVFxksXFEhjMlMPUrxf1ja7gibof1E49vZigAAAADjMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQICAgABNAAAAADgkwQAAAAAAMgAAAAAAAAABqHYF5E3VCqYNDe9/ip6slV/U1yKeHIraKSdwAAAAAADAgEEdAAAAACx+Xl4mhxH0TxI2HovJxcQ63+TJglRFzFikL1sKdr12UXlebz5JTz2i0ff8fs6OlwsIbrFsjwJrhKm4FVr8ItBAAAAAAAAAAAAAAAAAAAAAEXlebz5JTz2i0ff8fs6OlwsIbrFsjwJrhKm4FVr8ItB" ;
316210
317211 #[ test]
@@ -322,17 +216,29 @@ mod tests {
322216 println ! ( "Parsed instructions: {:?}" , parsed. instructions_data) ;
323217 println ! ( "Parsed JSON: {}" , serde_json:: to_string_pretty( & parsed) . unwrap( ) ) ;
324218
325- // Should combine CreateAccount + StakeInitialize into StakingActivate(MARINADE)
326- assert_eq ! ( parsed. instructions_data. len( ) , 1 , "Expected 1 combined instruction" ) ;
219+ // WASM returns raw instructions; combining is done in TypeScript
220+ // Expect: CreateAccount + StakeInitialize (2 instructions)
221+ assert_eq ! ( parsed. instructions_data. len( ) , 2 , "Expected 2 raw instructions" ) ;
327222
223+ // First instruction: CreateAccount
328224 match & parsed. instructions_data [ 0 ] {
329- ParsedInstruction :: StakingActivate ( params) => {
330- assert_eq ! ( params. staking_type, "MARINADE" ) ;
225+ ParsedInstruction :: CreateAccount ( params) => {
331226 assert_eq ! ( params. from_address, "5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe" ) ;
332- assert_eq ! ( params. staking_address , "7dRuGFbU2y2kijP6o1LYNzVyz4yf13MooqoionCzv5Za" ) ;
227+ assert_eq ! ( params. new_address , "7dRuGFbU2y2kijP6o1LYNzVyz4yf13MooqoionCzv5Za" ) ;
333228 assert_eq ! ( params. amount, "300000" ) ;
334229 }
335- other => panic ! ( "Expected StakingActivate(MARINADE) instruction, got {:?}" , other) ,
230+ other => panic ! ( "Expected CreateAccount instruction, got {:?}" , other) ,
231+ }
232+
233+ // Second instruction: StakeInitialize
234+ match & parsed. instructions_data [ 1 ] {
235+ ParsedInstruction :: StakeInitialize ( params) => {
236+ assert_eq ! ( params. staking_address, "7dRuGFbU2y2kijP6o1LYNzVyz4yf13MooqoionCzv5Za" ) ;
237+ // The staker is the authorized staker for Marinade
238+ assert_eq ! ( params. staker, "CyjoLt3kjqB57K7ewCBHmnHq3UgEj3ak6A7m6EsBsuhA" ) ;
239+ assert_eq ! ( params. withdrawer, "5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe" ) ;
240+ }
241+ other => panic ! ( "Expected StakeInitialize instruction, got {:?}" , other) ,
336242 }
337243 }
338244}
0 commit comments