Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules/
.idea/
*.iml
*.tsbuildinfo
.cursor/
4 changes: 4 additions & 0 deletions packages/wasm-solana/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/wasm-solana/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ solana-keypair = "2.0"
solana-signer = "2.0"
solana-signature = "3.0"
solana-address = "1.0"
# WASM random number generation support (need both 0.1 and 0.2 for all deps)
getrandom_01 = { package = "getrandom", version = "0.1", features = ["wasm-bindgen"] }
getrandom = { version = "0.2", features = ["js"] }
Comment on lines +33 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird

Copy link
Contributor Author

@lcovar lcovar Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed so it can create the random keypair for solana stake, weird indeed

# Serialization
bincode = "1.3"
borsh = "1.5"
Expand Down
Binary file modified packages/wasm-solana/bitgo-wasm-solana-0.0.1.tgz
Binary file not shown.
2 changes: 1 addition & 1 deletion packages/wasm-solana/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default tseslint.config(
{
languageOptions: {
parserOptions: {
projectService: true,
project: ["./tsconfig.json", "./tsconfig.test.json"],
tsconfigRootDir: import.meta.dirname,
},
},
Expand Down
27 changes: 25 additions & 2 deletions packages/wasm-solana/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,29 @@ export type { AddressLookupTableData } from "./versioned.js";
// Top-level function exports
export { parseTransaction } from "./parser.js";
export { buildTransaction, buildFromVersionedData } from "./builder.js";
export { buildFromIntent } from "./intentBuilder.js";

// Intent builder type exports
export type {
BaseIntent,
PaymentIntent,
StakeIntent,
UnstakeIntent,
ClaimIntent,
DeactivateIntent,
DelegateIntent,
EnableTokenIntent,
CloseAtaIntent,
ConsolidateIntent,
SolanaIntent,
StakePoolConfig,
BuildFromIntentParams,
BuildFromIntentResult,
GeneratedKeypair,
NonceSource,
BlockhashNonce,
DurableNonce,
} from "./intentBuilder.js";

// Program ID constants (from WASM)
export {
Expand All @@ -47,7 +70,7 @@ export type { AccountMeta, Instruction } from "./transaction.js";
export type {
TransactionInput,
ParsedTransaction,
DurableNonce,
DurableNonce as ParsedDurableNonce,
InstructionParams,
TransferParams,
CreateAccountParams,
Expand All @@ -74,7 +97,7 @@ export type {
// Builder type exports (prefixed to avoid conflict with parser/transaction types)
export type {
TransactionIntent,
NonceSource,
NonceSource as BuilderNonceSource,
BlockhashNonceSource,
DurableNonceSource,
AddressLookupTable as BuilderAddressLookupTable,
Expand Down
238 changes: 238 additions & 0 deletions packages/wasm-solana/js/intentBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/**
* High-level intent-based transaction building.
*
* This module provides `buildFromIntent()` which accepts BitGo intent objects
* directly and builds Solana transactions without requiring the caller to
* construct low-level instructions.
*
* The intent → transaction mapping happens entirely in Rust/WASM for simplicity.
*
* Usage:
* ```typescript
* import { buildFromIntent } from '@bitgo/wasm-solana';
*
* const result = buildFromIntent(intent, {
* feePayer: walletRootAddress,
* nonce: { type: 'blockhash', value: recentBlockhash },
* });
*
* // result.transaction - Transaction object
* // result.generatedKeypairs - any keypairs generated (e.g., stake accounts)
* ```
*/

import { IntentNamespace, WasmTransaction } from "./wasm/wasm_solana.js";
import { Transaction } from "./transaction.js";

/** Internal type for WASM result - matches what Rust returns */
interface WasmBuildResult {
transaction: WasmTransaction;
generatedKeypairs: GeneratedKeypair[];
}

// =============================================================================
// Types
// =============================================================================

/** Nonce source - blockhash or durable nonce */
export type NonceSource = BlockhashNonce | DurableNonce;

export interface BlockhashNonce {
type: "blockhash";
value: string;
}

export interface DurableNonce {
type: "durable";
address: string;
authority: string;
value: string;
}

/** Parameters for building a transaction from intent */
export interface BuildFromIntentParams {
/** Fee payer address (wallet root) */
feePayer: string;
/** Nonce source - blockhash or durable nonce */
nonce: NonceSource;
}

/** A keypair generated during transaction building */
export interface GeneratedKeypair {
/** Purpose of this keypair */
purpose: "stakeAccount" | "unstakeAccount" | "transferAuthority";
/** Public address (base58) */
address: string;
/** Secret key (base58) */
secretKey: string;
}

/** Result from building a transaction from intent */
export interface BuildFromIntentResult {
/** The built transaction */
transaction: Transaction;
/** Generated keypairs (for stake accounts, etc.) */
generatedKeypairs: GeneratedKeypair[];
}

// =============================================================================
// Intent Types (for TypeScript users)
// =============================================================================

/** Base intent - all intents have intentType */
export interface BaseIntent {
intentType: string;
memo?: string;
}

/** Payment intent */
export interface PaymentIntent extends BaseIntent {
intentType: "payment";
recipients?: Array<{
address?: { address: string };
amount?: { value: bigint; symbol?: string };
}>;
}

/** Stake intent */
export interface StakeIntent extends BaseIntent {
intentType: "stake";
validatorAddress: string;
amount?: { value: bigint };
stakingType?: "NATIVE" | "JITO" | "MARINADE";
stakePoolConfig?: StakePoolConfig;
}

/** Stake pool configuration (for Jito) */
export interface StakePoolConfig {
stakePoolAddress: string;
withdrawAuthority: string;
reserveStake: string;
destinationPoolAccount: string;
managerFeeAccount: string;
referralPoolAccount?: string;
poolMint: string;
validatorList?: string;
sourcePoolAccount?: string;
}

/** Unstake intent */
export interface UnstakeIntent extends BaseIntent {
intentType: "unstake";
stakingAddress: string;
validatorAddress?: string;
amount?: { value: bigint };
remainingStakingAmount?: { value: bigint };
stakingType?: "NATIVE" | "JITO" | "MARINADE";
stakePoolConfig?: StakePoolConfig;
}

/** Claim intent (withdraw from deactivated stake) */
export interface ClaimIntent extends BaseIntent {
intentType: "claim";
stakingAddress: string;
amount?: { value: bigint };
}

/** Deactivate intent */
export interface DeactivateIntent extends BaseIntent {
intentType: "deactivate";
stakingAddress?: string;
stakingAddresses?: string[];
}

/** Delegate intent */
export interface DelegateIntent extends BaseIntent {
intentType: "delegate";
validatorAddress: string;
stakingAddress?: string;
stakingAddresses?: string[];
}

/** Enable token intent (create ATA) */
export interface EnableTokenIntent extends BaseIntent {
intentType: "enableToken";
recipientAddress?: string;
tokenAddress?: string;
tokenProgramId?: string;
}

/** Close ATA intent */
export interface CloseAtaIntent extends BaseIntent {
intentType: "closeAssociatedTokenAccount";
tokenAccountAddress?: string;
tokenProgramId?: string;
}

/** Consolidate intent - transfer from child address to root */
export interface ConsolidateIntent extends BaseIntent {
intentType: "consolidate";
/** The child address to consolidate from (sender) */
receiveAddress: string;
/** Recipients (root address for SOL, ATAs for tokens) */
recipients?: Array<{
address?: { address: string };
amount?: { value: bigint };
}>;
}

/** Union of all supported intent types */
export type SolanaIntent =
| PaymentIntent
| StakeIntent
| UnstakeIntent
| ClaimIntent
| DeactivateIntent
| DelegateIntent
| EnableTokenIntent
| CloseAtaIntent
| ConsolidateIntent;

// =============================================================================
// Main Function
// =============================================================================

/**
* Build a Solana transaction from a BitGo intent.
*
* This function passes the intent directly to Rust/WASM which handles
* all the intent-to-transaction mapping internally.
*
* @param intent - The BitGo intent (with intentType, etc.)
* @param params - Build parameters (feePayer, nonce)
* @returns Transaction object and any generated keypairs
*
* @example
* ```typescript
* // Payment intent
* const result = buildFromIntent(
* {
* intentType: 'payment',
* recipients: [{ address: { address: recipient }, amount: { value: 1000000n } }]
* },
* { feePayer: walletRoot, nonce: { type: 'blockhash', value: blockhash } }
* );
*
* // Native staking - generates a new stake account keypair
* const result = buildFromIntent(
* {
* intentType: 'stake',
* validatorAddress: validator,
* amount: { value: 1000000000n }
* },
* { feePayer: walletRoot, nonce: { type: 'blockhash', value: blockhash } }
* );
* // result.generatedKeypairs[0] contains the stake account keypair
* ```
*/
export function buildFromIntent(
intent: BaseIntent,
params: BuildFromIntentParams,
): BuildFromIntentResult {
const result = IntentNamespace.build_from_intent(intent, params) as WasmBuildResult;

return {
transaction: Transaction.fromWasm(result.transaction),
generatedKeypairs: result.generatedKeypairs,
};
}
9 changes: 9 additions & 0 deletions packages/wasm-solana/js/keypair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ import { WasmKeypair } from "./wasm/wasm_solana.js";
export class Keypair {
private constructor(private _wasm: WasmKeypair) {}

/**
* Generate a new random keypair
* @returns A new Keypair instance with randomly generated keys
*/
static generate(): Keypair {
const wasm = WasmKeypair.generate();
return new Keypair(wasm);
}

/**
* Create a keypair from a 32-byte secret key
* @param secretKey - The 32-byte Ed25519 secret key
Expand Down
2 changes: 1 addition & 1 deletion packages/wasm-solana/src/builder/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ fn build_instruction(ix: IntentInstruction) -> Result<Instruction, WasmSolanaErr
"hex" => hex::decode(&data).map_err(|e| {
WasmSolanaError::new(&format!("Invalid hex data in custom instruction: {}", e))
})?,
"base64" | _ => {
_ => {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(&data)
Expand Down
4 changes: 2 additions & 2 deletions packages/wasm-solana/src/instructions/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ fn decode_system_instruction(ctx: InstructionContext) -> ParsedInstruction {
// This is part of CreateNonceAccount flow - parsed as intermediate NonceInitialize
// Will be combined with CreateAccount in post-processing
// Accounts: [0] nonce, [1] recent_blockhashes_sysvar, [2] rent_sysvar
if ctx.accounts.len() >= 1 {
if !ctx.accounts.is_empty() {
ParsedInstruction::NonceInitialize(NonceInitializeParams {
nonce_address: ctx.accounts[0].clone(),
auth_address: authority.to_string(),
Expand Down Expand Up @@ -141,7 +141,7 @@ fn decode_stake_instruction(ctx: InstructionContext) -> ParsedInstruction {
// This is part of StakingActivate flow - parsed as intermediate StakeInitialize
// Will be combined with CreateAccount + DelegateStake in post-processing
// Accounts: [0] stake, [1] rent_sysvar
if ctx.accounts.len() >= 1 {
if !ctx.accounts.is_empty() {
ParsedInstruction::StakeInitialize(StakeInitializeParams {
staking_address: ctx.accounts[0].clone(),
staker: authorized.staker.to_string(),
Expand Down
Loading