@@ -201,13 +201,13 @@ fn build_stake(
201201 let amount: u64 = intent. amount . as_ref ( ) . map ( |a| a. value ) . unwrap_or ( 0 ) ;
202202
203203 // Check if Jito staking
204- if intent. staking_type . as_deref ( ) == Some ( "JITO" ) {
204+ if intent. staking_type == Some ( StakingType :: Jito ) {
205205 if let Some ( config) = & intent. stake_pool_config {
206206 return build_jito_stake ( config, & fee_payer, amount) ;
207207 }
208208 }
209209
210- // Native staking: generate stake account keypair
210+ // Generate stake account keypair (used by both native and Marinade)
211211 let stake_keypair = Keypair :: new ( ) ;
212212 let stake_address = stake_keypair. address ( ) ;
213213 let stake_pubkey: Pubkey = stake_address
@@ -219,16 +219,45 @@ fn build_stake(
219219 . parse ( )
220220 . map_err ( |_| WasmSolanaError :: new ( "Invalid validatorAddress" ) ) ?;
221221
222+ // Marinade staking: CreateAccount + Initialize (no Delegate)
223+ // Staker authority is the validator, withdrawer is the user
224+ if intent. staking_type == Some ( StakingType :: Marinade ) {
225+ let instructions = vec ! [
226+ system_ix:: create_account(
227+ & fee_payer,
228+ & stake_pubkey,
229+ amount + STAKE_ACCOUNT_RENT ,
230+ STAKE_ACCOUNT_SPACE ,
231+ & solana_stake_interface:: program:: ID ,
232+ ) ,
233+ stake_ix:: initialize(
234+ & stake_pubkey,
235+ & Authorized {
236+ staker: validator_pubkey,
237+ withdrawer: fee_payer,
238+ } ,
239+ & Lockup :: default ( ) ,
240+ ) ,
241+ ] ;
242+
243+ let generated = vec ! [ GeneratedKeypair {
244+ purpose: "stakeAccount" . to_string( ) ,
245+ address: stake_address,
246+ secret_key: solana_sdk:: bs58:: encode( stake_keypair. secret_key_bytes( ) ) . into_string( ) ,
247+ } ] ;
248+
249+ return Ok ( ( instructions, generated) ) ;
250+ }
251+
252+ // Native staking: CreateAccount + Initialize + Delegate
222253 let instructions = vec ! [
223- // Create account
224254 system_ix:: create_account(
225255 & fee_payer,
226256 & stake_pubkey,
227257 amount + STAKE_ACCOUNT_RENT ,
228258 STAKE_ACCOUNT_SPACE ,
229259 & solana_stake_interface:: program:: ID ,
230260 ) ,
231- // Initialize stake
232261 stake_ix:: initialize(
233262 & stake_pubkey,
234263 & Authorized {
@@ -237,7 +266,6 @@ fn build_stake(
237266 } ,
238267 & Lockup :: default ( ) ,
239268 ) ,
240- // Delegate
241269 stake_ix:: delegate_stake( & stake_pubkey, & fee_payer, & validator_pubkey) ,
242270 ] ;
243271
@@ -344,13 +372,22 @@ fn build_unstake(
344372 . parse ( )
345373 . map_err ( |_| WasmSolanaError :: new ( "Invalid feePayer" ) ) ?;
346374
347- let stake_pubkey: Pubkey = intent
375+ // Marinade unstake: SystemProgram.transfer to recipient (no stake account involved)
376+ if intent. staking_type == Some ( StakingType :: Marinade ) {
377+ return build_marinade_unstake ( & intent, & fee_payer) ;
378+ }
379+
380+ // For native/Jito, staking_address is required
381+ let staking_address = intent
348382 . staking_address
383+ . as_ref ( )
384+ . ok_or_else ( || WasmSolanaError :: new ( "Missing stakingAddress for native/Jito unstake" ) ) ?;
385+ let stake_pubkey: Pubkey = staking_address
349386 . parse ( )
350387 . map_err ( |_| WasmSolanaError :: new ( "Invalid stakingAddress" ) ) ?;
351388
352389 // Check if Jito unstaking
353- if intent. staking_type . as_deref ( ) == Some ( "JITO" ) {
390+ if intent. staking_type == Some ( StakingType :: Jito ) {
354391 if let Some ( config) = & intent. stake_pool_config {
355392 let amount: u64 = intent. amount . as_ref ( ) . map ( |a| a. value ) . unwrap_or ( 0 ) ;
356393 return build_jito_unstake ( config, & fee_payer, & intent. validator_address , amount) ;
@@ -417,6 +454,42 @@ fn build_partial_unstake(
417454 Ok ( ( instructions, generated) )
418455}
419456
457+ fn build_marinade_unstake (
458+ intent : & UnstakeIntent ,
459+ fee_payer : & Pubkey ,
460+ ) -> Result < ( Vec < Instruction > , Vec < GeneratedKeypair > ) , WasmSolanaError > {
461+ let recipients = intent
462+ . recipients
463+ . as_ref ( )
464+ . ok_or_else ( || WasmSolanaError :: new ( "Missing recipients for Marinade unstake" ) ) ?;
465+
466+ if recipients. is_empty ( ) {
467+ return Err ( WasmSolanaError :: new (
468+ "Recipients array is empty for Marinade unstake" ,
469+ ) ) ;
470+ }
471+
472+ let recipient = & recipients[ 0 ] ;
473+ let to_address = recipient
474+ . address
475+ . as_ref ( )
476+ . map ( |a| & a. address )
477+ . ok_or_else ( || WasmSolanaError :: new ( "Recipient missing address for Marinade unstake" ) ) ?;
478+ let amount = recipient
479+ . amount
480+ . as_ref ( )
481+ . map ( |a| a. value )
482+ . ok_or_else ( || WasmSolanaError :: new ( "Recipient missing amount for Marinade unstake" ) ) ?;
483+
484+ let to_pubkey: Pubkey = to_address
485+ . parse ( )
486+ . map_err ( |_| WasmSolanaError :: new ( & format ! ( "Invalid recipient address: {}" , to_address) ) ) ?;
487+
488+ let instructions = vec ! [ system_ix:: transfer( fee_payer, & to_pubkey, amount) ] ;
489+
490+ Ok ( ( instructions, vec ! [ ] ) )
491+ }
492+
420493fn build_jito_unstake (
421494 config : & StakePoolConfig ,
422495 fee_payer : & Pubkey ,
@@ -1020,4 +1093,82 @@ mod tests {
10201093 let result = result. unwrap ( ) ;
10211094 assert ! ( result. generated_keypairs. is_empty( ) ) ;
10221095 }
1096+
1097+ #[ test]
1098+ fn test_build_marinade_stake_intent ( ) {
1099+ // Marinade stake: CreateAccount + Initialize (no Delegate)
1100+ // Staker = validator, Withdrawer = fee_payer
1101+ let intent = serde_json:: json!( {
1102+ "intentType" : "stake" ,
1103+ "validatorAddress" : "CyjoLt3kjqB57K7ewCBHmnHq3UgEj3ak6A7m6EsBsuhA" ,
1104+ "amount" : { "value" : "300000" } ,
1105+ "stakingType" : "MARINADE"
1106+ } ) ;
1107+
1108+ let result = build_from_intent ( & intent, & test_params ( ) ) ;
1109+ assert ! ( result. is_ok( ) , "Failed: {:?}" , result) ;
1110+ let result = result. unwrap ( ) ;
1111+
1112+ // Should generate a stake account keypair
1113+ assert_eq ! ( result. generated_keypairs. len( ) , 1 ) ;
1114+ assert_eq ! ( result. generated_keypairs[ 0 ] . purpose, "stakeAccount" ) ;
1115+
1116+ // Transaction should have 2 instructions (CreateAccount + Initialize)
1117+ // No Delegate instruction for Marinade
1118+ let msg = result. transaction . message ( ) ;
1119+ assert_eq ! (
1120+ msg. instructions. len( ) ,
1121+ 2 ,
1122+ "Marinade stake should have exactly 2 instructions (CreateAccount + Initialize)"
1123+ ) ;
1124+ }
1125+
1126+ #[ test]
1127+ fn test_build_marinade_unstake_intent ( ) {
1128+ // Marinade unstake: SystemProgram.transfer to recipient
1129+ let intent = serde_json:: json!( {
1130+ "intentType" : "unstake" ,
1131+ "stakingType" : "MARINADE" ,
1132+ "amount" : { "value" : "500000000000" } ,
1133+ "recipients" : [ {
1134+ "address" : { "address" : "opNS8ENpEMWdXcJUgJCsJTDp7arTXayoBEeBUg6UezP" } ,
1135+ "amount" : { "value" : "500000000000" }
1136+ } ] ,
1137+ "memo" : "{\" PrepareForRevoke\" :{\" user\" :\" DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB\" ,\" amount\" :\" 500000000000\" }}"
1138+ } ) ;
1139+
1140+ let result = build_from_intent ( & intent, & test_params ( ) ) ;
1141+ assert ! ( result. is_ok( ) , "Failed: {:?}" , result) ;
1142+ let result = result. unwrap ( ) ;
1143+
1144+ // No generated keypairs for Marinade unstake
1145+ assert ! ( result. generated_keypairs. is_empty( ) ) ;
1146+
1147+ // Transaction should have 1 transfer + 1 memo = 2 instructions
1148+ let msg = result. transaction . message ( ) ;
1149+ assert_eq ! (
1150+ msg. instructions. len( ) ,
1151+ 2 ,
1152+ "Marinade unstake should have transfer + memo instructions"
1153+ ) ;
1154+ }
1155+
1156+ #[ test]
1157+ fn test_build_marinade_unstake_requires_recipients ( ) {
1158+ let intent = serde_json:: json!( {
1159+ "intentType" : "unstake" ,
1160+ "stakingType" : "MARINADE" ,
1161+ "amount" : { "value" : "500000000000" }
1162+ } ) ;
1163+
1164+ let result = build_from_intent ( & intent, & test_params ( ) ) ;
1165+ assert ! ( result. is_err( ) , "Should fail without recipients" ) ;
1166+ assert ! (
1167+ result
1168+ . unwrap_err( )
1169+ . to_string( )
1170+ . contains( "Missing recipients" ) ,
1171+ "Error should mention missing recipients"
1172+ ) ;
1173+ }
10231174}
0 commit comments