Skip to content

Commit 2efb802

Browse files
committed
feat: add stake split instruction support
Add support for creating stake account split transactions to enable partial stake deactivation. Include parsing transaction signatures. Ticket: BTC-2955
1 parent 5bff3d8 commit 2efb802

File tree

7 files changed

+114
-6
lines changed

7 files changed

+114
-6
lines changed

packages/wasm-solana/js/builder.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,19 @@ export interface StakeAuthorizeInstruction {
175175
authority: string;
176176
}
177177

178+
/** Split stake account instruction (for partial deactivation) */
179+
export interface StakeSplitInstruction {
180+
type: "stakeSplit";
181+
/** Source stake account address (base58) */
182+
stake: string;
183+
/** Destination stake account (must be uninitialized/created first) (base58) */
184+
splitStake: string;
185+
/** Stake authority (base58) */
186+
authority: string;
187+
/** Amount in lamports to split (as string) */
188+
lamports: string;
189+
}
190+
178191
// =============================================================================
179192
// SPL Token Instructions
180193
// =============================================================================
@@ -338,6 +351,7 @@ export type Instruction =
338351
| StakeDeactivateInstruction
339352
| StakeWithdrawInstruction
340353
| StakeAuthorizeInstruction
354+
| StakeSplitInstruction
341355
| TokenTransferInstruction
342356
| CreateAssociatedTokenAccountInstruction
343357
| CloseAssociatedTokenAccountInstruction

packages/wasm-solana/js/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,14 @@ export type {
6868
StakeDeactivateInstruction,
6969
StakeWithdrawInstruction,
7070
StakeAuthorizeInstruction,
71+
StakeSplitInstruction,
7172
// SPL Token
7273
TokenTransferInstruction,
7374
CreateAssociatedTokenAccountInstruction,
7475
CloseAssociatedTokenAccountInstruction,
76+
MintToInstruction,
77+
BurnInstruction,
78+
ApproveInstruction,
7579
// Jito Stake Pool
7680
StakePoolDepositSolInstruction,
7781
StakePoolWithdrawStakeInstruction,

packages/wasm-solana/js/parser.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ export interface ParsedTransaction {
263263

264264
/** All account keys (base58 strings) */
265265
accountKeys: string[];
266+
267+
/** All signatures (base58 strings). Non-empty signatures indicate signed transaction. */
268+
signatures: string[];
266269
}
267270

268271
// =============================================================================

packages/wasm-solana/src/builder/build.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,32 @@ fn build_instruction(ix: IntentInstruction) -> Result<Instruction, WasmSolanaErr
372372
))
373373
}
374374

375+
IntentInstruction::StakeSplit {
376+
stake,
377+
split_stake,
378+
authority,
379+
lamports,
380+
} => {
381+
let stake_pubkey: Pubkey = stake.parse().map_err(|_| {
382+
WasmSolanaError::new(&format!("Invalid stakeSplit.stake: {}", stake))
383+
})?;
384+
let split_stake_pubkey: Pubkey = split_stake.parse().map_err(|_| {
385+
WasmSolanaError::new(&format!("Invalid stakeSplit.splitStake: {}", split_stake))
386+
})?;
387+
let authority_pubkey: Pubkey = authority.parse().map_err(|_| {
388+
WasmSolanaError::new(&format!("Invalid stakeSplit.authority: {}", authority))
389+
})?;
390+
let amount: u64 = lamports.parse().map_err(|_| {
391+
WasmSolanaError::new(&format!("Invalid stakeSplit.lamports: {}", lamports))
392+
})?;
393+
Ok(build_stake_split(
394+
&stake_pubkey,
395+
&split_stake_pubkey,
396+
&authority_pubkey,
397+
amount,
398+
))
399+
}
400+
375401
// ===== SPL Token Program =====
376402
IntentInstruction::TokenTransfer {
377403
source,
@@ -859,6 +885,25 @@ fn build_stake_authorize(
859885
)
860886
}
861887

888+
/// Build a stake split instruction.
889+
/// Used for partial stake deactivation - splits lamports from one stake account to another.
890+
fn build_stake_split(
891+
stake: &Pubkey,
892+
split_stake: &Pubkey,
893+
authority: &Pubkey,
894+
lamports: u64,
895+
) -> Instruction {
896+
Instruction::new_with_bincode(
897+
program_ids::stake_program(),
898+
&StakeInstruction::Split(lamports),
899+
vec![
900+
AccountMeta::new(*stake, false), // Source stake account
901+
AccountMeta::new(*split_stake, false), // Destination stake account
902+
AccountMeta::new_readonly(*authority, true), // Stake authority
903+
],
904+
)
905+
}
906+
862907
// ===== SPL Token Instruction Builders =====
863908

864909
/// Build a TransferChecked instruction for SPL Token.
@@ -1454,4 +1499,28 @@ mod tests {
14541499
);
14551500
verify_tx_structure(&result.unwrap(), 1);
14561501
}
1502+
1503+
#[test]
1504+
fn test_build_stake_split() {
1505+
let intent = TransactionIntent {
1506+
fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(),
1507+
nonce: Nonce::Blockhash {
1508+
value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(),
1509+
},
1510+
instructions: vec![IntentInstruction::StakeSplit {
1511+
stake: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(),
1512+
split_stake: "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN".to_string(),
1513+
authority: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(),
1514+
lamports: "500000000".to_string(), // 0.5 SOL
1515+
}],
1516+
};
1517+
1518+
let result = build_transaction(intent);
1519+
assert!(
1520+
result.is_ok(),
1521+
"Failed to build stake split: {:?}",
1522+
result
1523+
);
1524+
verify_tx_structure(&result.unwrap(), 1);
1525+
}
14571526
}

packages/wasm-solana/src/builder/types.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,19 @@ pub enum Instruction {
153153
authority: String,
154154
},
155155

156+
/// Split stake account (used for partial deactivation)
157+
StakeSplit {
158+
/// Source stake account address
159+
stake: String,
160+
/// Destination stake account (must be uninitialized/created first)
161+
#[serde(rename = "splitStake")]
162+
split_stake: String,
163+
/// Stake authority
164+
authority: String,
165+
/// Amount in lamports to split (as string)
166+
lamports: String,
167+
},
168+
156169
// ===== SPL Token Instructions =====
157170
/// Transfer tokens (uses TransferChecked for safety)
158171
TokenTransfer {

packages/wasm-solana/src/parser.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ pub struct ParsedTransaction {
3535

3636
/// All account keys (base58 strings).
3737
pub account_keys: Vec<String>,
38+
39+
/// All signatures (base58 strings). Non-empty signatures indicate signed transaction.
40+
pub signatures: Vec<String>,
3841
}
3942

4043
/// Durable nonce information for nonce-based transactions.
@@ -64,7 +67,8 @@ impl TryIntoJsValue for ParsedTransaction {
6467
"nonce" => self.nonce,
6568
"durableNonce" => self.durable_nonce,
6669
"instructionsData" => self.instructions_data,
67-
"accountKeys" => self.account_keys
70+
"accountKeys" => self.account_keys,
71+
"signatures" => self.signatures
6872
)
6973
}
7074
}
@@ -139,13 +143,17 @@ pub fn parse_transaction(bytes: &[u8]) -> Result<ParsedTransaction, String> {
139143
// (which is the nonce value from the nonce account)
140144
let nonce = message.recent_blockhash.to_string();
141145

146+
// Extract signatures as base58 strings
147+
let signatures: Vec<String> = tx.signatures.iter().map(|s| s.to_string()).collect();
148+
142149
Ok(ParsedTransaction {
143150
fee_payer,
144151
num_signatures: message.header.num_required_signatures,
145152
nonce,
146153
durable_nonce,
147154
instructions_data,
148155
account_keys,
156+
signatures,
149157
})
150158
}
151159

packages/wasm-solana/test/transaction.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,7 @@ describe("Transaction", () => {
166166

167167
// Try to add a signature with wrong length
168168
const badSignature = new Uint8Array(32);
169-
assert.throws(
170-
() => tx.addSignature(feePayer, badSignature),
171-
/Invalid signature length/
172-
);
169+
assert.throws(() => tx.addSignature(feePayer, badSignature), /Invalid signature length/);
173170
});
174171

175172
it("should throw for non-signer pubkey", () => {
@@ -179,7 +176,7 @@ describe("Transaction", () => {
179176
// Try to add signature for non-signer (System program)
180177
assert.throws(
181178
() => tx.addSignature("11111111111111111111111111111111", signature),
182-
/not a required signer/
179+
/not a required signer/,
183180
);
184181
});
185182

0 commit comments

Comments
 (0)