Skip to content

Commit c917612

Browse files
committed
refactor(wasm-solana): move instruction combining from Rust to TypeScript
- Remove combine_instructions function from Rust parser - Add intermediate types (NonceInitialize, StakeInitialize) to TypeScript - Raw instructions now returned from WASM, combining done by consumer - This enables all combining logic to be in one place (TypeScript)
1 parent 503ed48 commit c917612

2 files changed

Lines changed: 45 additions & 122 deletions

File tree

packages/wasm-solana/js/parser.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface NonceAdvanceParams {
3636
authWalletAddress: string;
3737
}
3838

39-
/** Create nonce account parameters */
39+
/** Create nonce account parameters (combined type) */
4040
export interface CreateNonceAccountParams {
4141
type: "CreateNonceAccount";
4242
fromAddress: string;
@@ -45,7 +45,22 @@ export interface CreateNonceAccountParams {
4545
amount: string;
4646
}
4747

48-
/** Staking activate parameters */
48+
/** Nonce initialize parameters (intermediate - combined into CreateNonceAccount) */
49+
export interface NonceInitializeParams {
50+
type: "NonceInitialize";
51+
nonceAddress: string;
52+
authAddress: string;
53+
}
54+
55+
/** Stake initialize parameters (intermediate - combined into StakingActivate) */
56+
export interface StakeInitializeParams {
57+
type: "StakeInitialize";
58+
stakingAddress: string;
59+
staker: string;
60+
withdrawer: string;
61+
}
62+
63+
/** Staking activate parameters (combined type) */
4964
export interface StakingActivateParams {
5065
type: "StakingActivate";
5166
fromAddress: string;
@@ -186,11 +201,13 @@ export type InstructionParams =
186201
| CreateAccountParams
187202
| NonceAdvanceParams
188203
| CreateNonceAccountParams
204+
| NonceInitializeParams
189205
| StakingActivateParams
190206
| StakingDeactivateParams
191207
| StakingWithdrawParams
192208
| StakingDelegateParams
193209
| StakingAuthorizeParams
210+
| StakeInitializeParams
194211
| SetComputeUnitLimitParams
195212
| SetPriorityFeeParams
196213
| TokenTransferParams

packages/wasm-solana/src/parser.rs

Lines changed: 26 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@
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};
1511
use crate::transaction::{Transaction, TransactionExt};
1612
use 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.
251144
mod 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

Comments
 (0)