Skip to content

feat(sdk-coin-sol): implement staking activate for jito #6502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
102 changes: 102 additions & 0 deletions examples/ts/sol/stake-jito.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Stakes JitoSOL tokens on Solana devnet.
*
* Copyright 2025, BitGo, Inc. All Rights Reserved.
*/
import { BitGoAPI } from '@bitgo/sdk-api'
import { TransactionBuilderFactory, Tsol } from '@bitgo/sdk-coin-sol'
import { coins } from '@bitgo/statics'
import { Connection, PublicKey, clusterApiUrl, Transaction, Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js"
import { getStakePoolAccount, updateStakePool } from '@solana/spl-stake-pool'
import { BinTools } from 'avalanche';

require('dotenv').config({ path: '../../.env' })

const AMOUNT_LAMPORTS = 1000
const JITO_STAKE_POOL_ADDRESS = 'Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb'
const NETWORK = 'devnet'

const bitgo = new BitGoAPI({
accessToken: process.env.TESTNET_ACCESS_TOKEN,
env: 'test',
})
const coin = coins.get("tsol")
bitgo.register(coin.name, Tsol.createInstance)

async function main() {
const account = getAccount()
const connection = new Connection(clusterApiUrl(NETWORK), 'confirmed')
const recentBlockhash = await connection.getLatestBlockhash()
const stakePoolAccount = await getStakePoolAccount(connection, new PublicKey(JITO_STAKE_POOL_ADDRESS))

// Account should have sufficient balance
const accountBalance = await connection.getBalance(account.publicKey)
if (accountBalance < 0.1 * LAMPORTS_PER_SOL) {
console.info(`Your account balance is ${accountBalance / LAMPORTS_PER_SOL} SOL, requesting airdrop`)
const sig = await connection.requestAirdrop(account.publicKey, 2 * LAMPORTS_PER_SOL)
await connection.confirmTransaction(sig)
console.info(`Airdrop successful: ${sig}`)
}

// Stake pool should be up to date
const epochInfo = await connection.getEpochInfo()
if (stakePoolAccount.account.data.lastUpdateEpoch.ltn(epochInfo.epoch)) {
console.info('Stake pool is out of date.')
const usp = await updateStakePool(connection, stakePoolAccount)
const tx = new Transaction()
tx.add(...usp.updateListInstructions, ...usp.finalInstructions)
const signer = Keypair.fromSecretKey(account.secretKeyArray)
const sig = await connection.sendTransaction(tx, [signer])
await connection.confirmTransaction(sig)
console.info(`Stake pool updated: ${sig}`)
}

// Use BitGoAPI to build depositSol instruction
const txBuilder = new TransactionBuilderFactory(coin).getStakingActivateBuilder()
txBuilder
.amount(`${AMOUNT_LAMPORTS}`)
.sender(account.publicKey.toBase58())
.stakingAddress(JITO_STAKE_POOL_ADDRESS)
.validator(JITO_STAKE_POOL_ADDRESS)
.isJito(true)
.nonce(recentBlockhash.blockhash)
txBuilder.sign({ key: account.secretKey })
const tx = await txBuilder.build()
const serializedTx = tx.toBroadcastFormat()
console.info(`Transaction JSON:\n${JSON.stringify(tx.toJson(), undefined, 2)}`)

// Send transaction
try {
const sig = await connection.sendRawTransaction(Buffer.from(serializedTx, 'base64'))
await connection.confirmTransaction(sig)
console.log(`${AMOUNT_LAMPORTS / LAMPORTS_PER_SOL} SOL deposited`, sig)
} catch (e) {
console.log('Error sending transaction')
console.error(e)
if (e.transactionMessage === 'Transaction simulation failed: Error processing Instruction 0: Provided owner is not allowed') {
console.error('If you successfully staked JitoSOL once, you cannot stake again.')
}
}
}

const getAccount = () => {
const publicKey = process.env.ACCOUNT_PUBLIC_KEY
const secretKey = process.env.ACCOUNT_SECRET_KEY
const secretKeyArray = process.env.ACCOUNT_SECRET_KEY_ARRAY && JSON.parse(process.env.ACCOUNT_SECRET_KEY_ARRAY)
if (publicKey === undefined || secretKey === undefined || secretKeyArray === undefined) {
const { publicKey, secretKey } = Keypair.generate()
console.log('Here is a new account to save into your .env file.')
console.log(`ACCOUNT_PUBLIC_KEY=${publicKey.toBase58()}`)
console.log(`ACCOUNT_SECRET_KEY_ARRAY=${JSON.stringify(Array.from(secretKey))}`)
console.log(`ACCOUNT_SECRET_KEY=${BinTools.getInstance().bufferToB58(BinTools.getInstance().fromArrayBufferToBuffer(secretKey))}`)
throw new Error("Missing account information")
}

return {
publicKey: new PublicKey(publicKey),
secretKey,
secretKeyArray: new Uint8Array(secretKeyArray),
}
}

main().catch((e) => console.error(e))
15 changes: 15 additions & 0 deletions modules/sdk-coin-sol/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export const STAKE_ACCOUNT_RENT_EXEMPT_AMOUNT = 2282880;

export const UNAVAILABLE_TEXT = 'UNAVAILABLE';

export const JITO_STAKE_POOL_ADDRESS = 'Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb';
export const JITOSOL_MINT_ADDRESS = 'J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn';
export const JITO_STAKE_POOL_RESERVE_ACCOUNT = 'BgKUXdS29YcHCFrPm5M8oLHiTzZaMDjsebggjoaQ6KFL';
export const JITO_STAKE_POOL_RESERVE_ACCOUNT_TESTNET = 'rrWBQqRqBXYZw3CmPCCcjFxQ2Ds4JFJd7oRQJ997dhz';
export const JITO_MANAGER_FEE_ACCOUNT = 'feeeFLLsam6xZJFc6UQFrHqkvVt4jfmVvi2BRLkUZ4i';
export const JITO_MANAGER_FEE_ACCOUNT_TESTNET = 'DH7tmjoQ5zjqcgfYJU22JqmXhP5EY1tkbYpgVWUS2oNo';

// Sdk instructions, mainly to check decoded types.
export enum ValidInstructionTypesEnum {
AdvanceNonceAccount = 'AdvanceNonceAccount',
Expand All @@ -28,6 +35,7 @@ export enum ValidInstructionTypesEnum {
Split = 'Split',
Authorize = 'Authorize',
SetPriorityFee = 'SetPriorityFee',
DepositSol = 'DepositSol',
}

// Internal instructions types
Expand Down Expand Up @@ -65,6 +73,7 @@ export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [
ValidInstructionTypesEnum.Split,
ValidInstructionTypesEnum.Authorize,
ValidInstructionTypesEnum.SetPriorityFee,
ValidInstructionTypesEnum.DepositSol,
];

/** Const to check the order of the Wallet Init instructions when decode */
Expand All @@ -89,6 +98,12 @@ export const marinadeStakingActivateInstructionsIndexes = {
Memo: 2,
} as const;

/** Const to check the order of the Jito Staking Activate instructions when decode */
export const jitoStakingActivateInstructionsIndexes = {
AtaInit: 0,
DepositSol: 1,
} as const;

/** Const to check the order of the Staking Authorize instructions when decode */
export const stakingAuthorizeInstructionsIndexes = {
Authorize: 0,
Expand Down
119 changes: 119 additions & 0 deletions modules/sdk-coin-sol/src/lib/future/depositSol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* @file Implementation of the depositSol instruction. On upgrade of
* '@solana/spl-token', this module may no longer be necessary.
*/

import { StakePoolInstruction, STAKE_POOL_PROGRAM_ID, DepositSolParams } from '@solana/spl-stake-pool';
import {
createAssociatedTokenAccountInstruction,
getAssociatedTokenAddressSync,
TOKEN_PROGRAM_ID,
} from '@solana/spl-token';
import { AccountMeta, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js';
import assert from 'assert';

export const DEPOSIT_SOL_LAYOUT_CODE = 14;

export interface DepositSolInstructionsParams {
stakePoolAddress: PublicKey;
from: PublicKey;
lamports: bigint;
}

/**
* Construct Solana depositSol stake pool instruction from parameters.
*
* @param {DepositSolInstructionsParams} params - parameters for staking to stake pool
* @param poolMint - pool mint derived from getStakePoolAccount
* @param reserveStake - reserve account derived from getStakePoolAccount
* @param managerFeeAccount - manager fee account derived from getStakePoolAccount
* @returns {TransactionInstruction}
*/
export function depositSolInstructions(
params: DepositSolInstructionsParams,
poolMint: PublicKey,
reserveStake: PublicKey,
managerFeeAccount: PublicKey
): TransactionInstruction[] {
const { stakePoolAddress, from, lamports } = params;

// findWithdrawAuthorityProgramAddress
const [withdrawAuthority] = PublicKey.findProgramAddressSync(
[stakePoolAddress.toBuffer(), Buffer.from('withdraw')],
STAKE_POOL_PROGRAM_ID
);

const associatedAddress = getAssociatedTokenAddressSync(poolMint, from);

return [
createAssociatedTokenAccountInstruction(from, associatedAddress, from, poolMint),
StakePoolInstruction.depositSol({
stakePool: stakePoolAddress,
reserveStake,
fundingAccount: from,
destinationPoolAccount: associatedAddress,
managerFeeAccount: managerFeeAccount,
referralPoolAccount: associatedAddress,
poolMint: poolMint,
lamports: Number(lamports),
withdrawAuthority,
}),
];
}

function parseKey(key: AccountMeta, template: { isSigner: boolean; isWritable: boolean }): PublicKey {
assert(
key.isSigner === template.isSigner && key.isWritable === template.isWritable,
'Unexpected key metadata in DepositSol instruction'
);
return key.pubkey;
}

/**
* Construct Solana depositSol stake pool parameters from instruction.
*
* @param {TransactionInstruction} instruction
* @returns {DepositSolParams}
*/
export function decodeDepositSol(instruction: TransactionInstruction): DepositSolParams {
Copy link
Author

Choose a reason for hiding this comment

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

const { programId, keys, data } = instruction;

assert(
programId.equals(STAKE_POOL_PROGRAM_ID),
'Invalid DepositSol instruction, program ID must be the Stake Pool Program'
);

let i = 0;
const stakePool = parseKey(keys[i++], { isSigner: false, isWritable: true });
const withdrawAuthority = parseKey(keys[i++], { isSigner: false, isWritable: false });
const reserveStake = parseKey(keys[i++], { isSigner: false, isWritable: true });
const fundingAccount = parseKey(keys[i++], { isSigner: true, isWritable: true });
const destinationPoolAccount = parseKey(keys[i++], { isSigner: false, isWritable: true });
const managerFeeAccount = parseKey(keys[i++], { isSigner: false, isWritable: true });
const referralPoolAccount = parseKey(keys[i++], { isSigner: false, isWritable: true });
const poolMint = parseKey(keys[i++], { isSigner: false, isWritable: true });
const systemProgramProgramId = parseKey(keys[i++], { isSigner: false, isWritable: false });
assert(systemProgramProgramId.equals(SystemProgram.programId), 'Unexpected pubkey in DepositSol instruction');
const tokenProgramId = parseKey(keys[i++], { isSigner: false, isWritable: false });
assert(tokenProgramId.equals(TOKEN_PROGRAM_ID), 'Unexpected pubkey in DepositSol instruction');
const depositAuthority = keys.length > 10 ? parseKey(keys[i++], { isSigner: true, isWritable: false }) : undefined;
assert(keys.length <= 11, 'Too many keys in DepositSol instruction');

const layoutCode = data.readUint8(0);
assert(layoutCode === DEPOSIT_SOL_LAYOUT_CODE, 'Incorrect layout code in DepositSol data');
assert(data.length === 9, 'Incorrect data size for DepositSol layout');
const lamports = data.readBigInt64LE(1);

return {
stakePool,
depositAuthority,
withdrawAuthority,
reserveStake,
fundingAccount,
destinationPoolAccount,
managerFeeAccount,
referralPoolAccount,
poolMint,
lamports: Number(lamports),
};
}
4 changes: 4 additions & 0 deletions modules/sdk-coin-sol/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TransactionExplanation as BaseTransactionExplanation, Recipient } from
import { DecodedCloseAccountInstruction } from '@solana/spl-token';
import { Blockhash, StakeInstructionType, SystemInstructionType, TransactionSignature } from '@solana/web3.js';
import { InstructionBuilderTypes } from './constants';
import { StakePoolInstructionType } from '@solana/spl-stake-pool';

// TODO(STLX-9890): Add the interfaces for validityWindow and SequenceId
export interface SolanaKeys {
Expand Down Expand Up @@ -86,6 +87,8 @@ export interface StakingActivate {
amount: string;
validator: string;
isMarinade?: boolean;
isJito?: boolean;
isTestnet?: boolean;
};
}

Expand Down Expand Up @@ -149,6 +152,7 @@ export interface AtaClose {
export type ValidInstructionTypes =
| SystemInstructionType
| StakeInstructionType
| StakePoolInstructionType
| 'Memo'
| 'InitializeAssociatedTokenAccount'
| 'CloseAssociatedTokenAccount'
Expand Down
Loading
Loading