Skip to content

Commit 634a453

Browse files
committed
feat(sdk-coin-sol): implement staking activate for jito
TICKET: SC-2314
1 parent af07a21 commit 634a453

File tree

14 files changed

+577
-36
lines changed

14 files changed

+577
-36
lines changed

examples/ts/sol/stake-jito.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Stakes JitoSOL tokens on Solana devnet.
3+
*
4+
* Copyright 2025, BitGo, Inc. All Rights Reserved.
5+
*/
6+
import { BitGoAPI } from '@bitgo/sdk-api'
7+
import { TransactionBuilderFactory, Tsol } from '@bitgo/sdk-coin-sol'
8+
import { coins } from '@bitgo/statics'
9+
import { Connection, PublicKey, clusterApiUrl, Transaction, Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js"
10+
import { getStakePoolAccount, updateStakePool } from '@solana/spl-stake-pool'
11+
import { BinTools } from 'avalanche';
12+
13+
require('dotenv').config({ path: '../../.env' })
14+
15+
const AMOUNT_LAMPORTS = 1000
16+
const JITO_STAKE_POOL_ADDRESS = 'Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb'
17+
const NETWORK = 'devnet'
18+
19+
const bitgo = new BitGoAPI({
20+
accessToken: process.env.TESTNET_ACCESS_TOKEN,
21+
env: 'test',
22+
})
23+
const coin = coins.get("tsol")
24+
bitgo.register(coin.name, Tsol.createInstance)
25+
26+
async function main() {
27+
const account = getAccount()
28+
const connection = new Connection(clusterApiUrl(NETWORK), 'confirmed')
29+
const recentBlockhash = await connection.getLatestBlockhash()
30+
const stakePoolAccount = await getStakePoolAccount(connection, new PublicKey(JITO_STAKE_POOL_ADDRESS))
31+
32+
// Account should have sufficient balance
33+
const accountBalance = await connection.getBalance(account.publicKey)
34+
if (accountBalance < 0.1 * LAMPORTS_PER_SOL) {
35+
console.info(`Your account balance is ${accountBalance / LAMPORTS_PER_SOL} SOL, requesting airdrop`)
36+
const sig = await connection.requestAirdrop(account.publicKey, 2 * LAMPORTS_PER_SOL)
37+
await connection.confirmTransaction(sig)
38+
console.info(`Airdrop successful: ${sig}`)
39+
}
40+
41+
// Stake pool should be up to date
42+
const epochInfo = await connection.getEpochInfo()
43+
if (stakePoolAccount.account.data.lastUpdateEpoch.ltn(epochInfo.epoch)) {
44+
console.info('Stake pool is out of date.')
45+
const usp = await updateStakePool(connection, stakePoolAccount)
46+
const tx = new Transaction()
47+
tx.add(...usp.updateListInstructions, ...usp.finalInstructions)
48+
const signer = Keypair.fromSecretKey(account.secretKeyArray)
49+
const sig = await connection.sendTransaction(tx, [signer])
50+
await connection.confirmTransaction(sig)
51+
console.info(`Stake pool updated: ${sig}`)
52+
}
53+
54+
// Use BitGoAPI to build depositSol instruction
55+
const txBuilder = new TransactionBuilderFactory(coin).getStakingActivateBuilder()
56+
txBuilder
57+
.amount(`${AMOUNT_LAMPORTS}`)
58+
.sender(account.publicKey.toBase58())
59+
.stakingAddress(JITO_STAKE_POOL_ADDRESS)
60+
.validator(JITO_STAKE_POOL_ADDRESS)
61+
.isJito(true)
62+
.nonce(recentBlockhash.blockhash)
63+
txBuilder.sign({ key: account.secretKey })
64+
const tx = await txBuilder.build()
65+
const serializedTx = tx.toBroadcastFormat()
66+
console.info(`Transaction JSON:\n${JSON.stringify(tx.toJson(), undefined, 2)}`)
67+
68+
// Send transaction
69+
try {
70+
const sig = await connection.sendRawTransaction(Buffer.from(serializedTx, 'base64'))
71+
await connection.confirmTransaction(sig)
72+
console.log(`${AMOUNT_LAMPORTS / LAMPORTS_PER_SOL} SOL deposited`, sig)
73+
} catch (e) {
74+
console.log('Error sending transaction')
75+
console.error(e)
76+
if (e.transactionMessage === 'Transaction simulation failed: Error processing Instruction 0: Provided owner is not allowed') {
77+
console.error('If you successfully staked JitoSOL once, you cannot stake again.')
78+
}
79+
}
80+
}
81+
82+
const getAccount = () => {
83+
const publicKey = process.env.ACCOUNT_PUBLIC_KEY
84+
const secretKey = process.env.ACCOUNT_SECRET_KEY
85+
const secretKeyArray = process.env.ACCOUNT_SECRET_KEY_ARRAY && JSON.parse(process.env.ACCOUNT_SECRET_KEY_ARRAY)
86+
if (publicKey === undefined || secretKey === undefined || secretKeyArray === undefined) {
87+
const { publicKey, secretKey } = Keypair.generate()
88+
console.log('Here is a new account to save into your .env file.')
89+
console.log(`ACCOUNT_PUBLIC_KEY=${publicKey.toBase58()}`)
90+
console.log(`ACCOUNT_SECRET_KEY_ARRAY=${JSON.stringify(Array.from(secretKey))}`)
91+
console.log(`ACCOUNT_SECRET_KEY=${BinTools.getInstance().bufferToB58(BinTools.getInstance().fromArrayBufferToBuffer(secretKey))}`)
92+
throw new Error("Missing account information")
93+
}
94+
95+
return {
96+
publicKey: new PublicKey(publicKey),
97+
secretKey,
98+
secretKeyArray: new Uint8Array(secretKeyArray),
99+
}
100+
}
101+
102+
main().catch((e) => console.error(e))

modules/sdk-coin-sol/src/lib/constants.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ export const STAKE_ACCOUNT_RENT_EXEMPT_AMOUNT = 2282880;
99

1010
export const UNAVAILABLE_TEXT = 'UNAVAILABLE';
1111

12+
export const JITO_STAKE_POOL_ADDRESS = 'Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb';
13+
export const JITOSOL_MINT_ADDRESS = 'J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn';
14+
export const JITO_STAKE_POOL_RESERVE_ACCOUNT = 'BgKUXdS29YcHCFrPm5M8oLHiTzZaMDjsebggjoaQ6KFL';
15+
export const JITO_STAKE_POOL_RESERVE_ACCOUNT_TESTNET = 'rrWBQqRqBXYZw3CmPCCcjFxQ2Ds4JFJd7oRQJ997dhz';
16+
export const JITO_MANAGER_FEE_ACCOUNT = 'feeeFLLsam6xZJFc6UQFrHqkvVt4jfmVvi2BRLkUZ4i';
17+
export const JITO_MANAGER_FEE_ACCOUNT_TESTNET = 'DH7tmjoQ5zjqcgfYJU22JqmXhP5EY1tkbYpgVWUS2oNo';
18+
1219
// Sdk instructions, mainly to check decoded types.
1320
export enum ValidInstructionTypesEnum {
1421
AdvanceNonceAccount = 'AdvanceNonceAccount',
@@ -28,6 +35,7 @@ export enum ValidInstructionTypesEnum {
2835
Split = 'Split',
2936
Authorize = 'Authorize',
3037
SetPriorityFee = 'SetPriorityFee',
38+
DepositSol = 'DepositSol',
3139
}
3240

3341
// Internal instructions types
@@ -65,6 +73,7 @@ export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [
6573
ValidInstructionTypesEnum.Split,
6674
ValidInstructionTypesEnum.Authorize,
6775
ValidInstructionTypesEnum.SetPriorityFee,
76+
ValidInstructionTypesEnum.DepositSol,
6877
];
6978

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

101+
/** Const to check the order of the Jito Staking Activate instructions when decode */
102+
export const jitoStakingActivateInstructionsIndexes = {
103+
AtaInit: 0,
104+
DepositSol: 1,
105+
} as const;
106+
92107
/** Const to check the order of the Staking Authorize instructions when decode */
93108
export const stakingAuthorizeInstructionsIndexes = {
94109
Authorize: 0,
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* @file Implementation of the depositSol instruction. On upgrade of
3+
* '@solana/spl-token', this module may no longer be necessary.
4+
*/
5+
6+
import { StakePoolInstruction, STAKE_POOL_PROGRAM_ID, DepositSolParams } from '@solana/spl-stake-pool';
7+
import {
8+
createAssociatedTokenAccountInstruction,
9+
getAssociatedTokenAddressSync,
10+
TOKEN_PROGRAM_ID,
11+
} from '@solana/spl-token';
12+
import { AccountMeta, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js';
13+
import assert from 'assert';
14+
15+
export const DEPOSIT_SOL_LAYOUT_CODE = 14;
16+
17+
export interface DepositSolInstructionsParams {
18+
stakePoolAddress: PublicKey;
19+
from: PublicKey;
20+
lamports: bigint;
21+
}
22+
23+
/**
24+
* Construct Solana depositSol stake pool instruction from parameters.
25+
*
26+
* @param {DepositSolInstructionsParams} params - parameters for staking to stake pool
27+
* @param poolMint - pool mint derived from getStakePoolAccount
28+
* @param reserveStake - reserve account derived from getStakePoolAccount
29+
* @param managerFeeAccount - manager fee account derived from getStakePoolAccount
30+
* @returns {TransactionInstruction}
31+
*/
32+
export function depositSolInstructions(
33+
params: DepositSolInstructionsParams,
34+
poolMint: PublicKey,
35+
reserveStake: PublicKey,
36+
managerFeeAccount: PublicKey
37+
): TransactionInstruction[] {
38+
const { stakePoolAddress, from, lamports } = params;
39+
40+
// findWithdrawAuthorityProgramAddress
41+
const [withdrawAuthority] = PublicKey.findProgramAddressSync(
42+
[stakePoolAddress.toBuffer(), Buffer.from('withdraw')],
43+
STAKE_POOL_PROGRAM_ID
44+
);
45+
46+
const associatedAddress = getAssociatedTokenAddressSync(poolMint, from);
47+
48+
return [
49+
createAssociatedTokenAccountInstruction(from, associatedAddress, from, poolMint),
50+
StakePoolInstruction.depositSol({
51+
stakePool: stakePoolAddress,
52+
reserveStake,
53+
fundingAccount: from,
54+
destinationPoolAccount: associatedAddress,
55+
managerFeeAccount: managerFeeAccount,
56+
referralPoolAccount: associatedAddress,
57+
poolMint: poolMint,
58+
lamports: Number(lamports),
59+
withdrawAuthority,
60+
}),
61+
];
62+
}
63+
64+
function parseKey(key: AccountMeta, template: { isSigner: boolean; isWritable: boolean }): PublicKey {
65+
assert(
66+
key.isSigner === template.isSigner && key.isWritable === template.isWritable,
67+
'Unexpected key metadata in DepositSol instruction'
68+
);
69+
return key.pubkey;
70+
}
71+
72+
/**
73+
* Construct Solana depositSol stake pool parameters from instruction.
74+
*
75+
* @param {TransactionInstruction} instruction
76+
* @returns {DepositSolParams}
77+
*/
78+
export function decodeDepositSol(instruction: TransactionInstruction): DepositSolParams {
79+
const { programId, keys, data } = instruction;
80+
81+
assert(
82+
programId.equals(STAKE_POOL_PROGRAM_ID),
83+
'Invalid DepositSol instruction, program ID must be the Stake Pool Program'
84+
);
85+
86+
let i = 0;
87+
const stakePool = parseKey(keys[i++], { isSigner: false, isWritable: true });
88+
const withdrawAuthority = parseKey(keys[i++], { isSigner: false, isWritable: false });
89+
const reserveStake = parseKey(keys[i++], { isSigner: false, isWritable: true });
90+
const fundingAccount = parseKey(keys[i++], { isSigner: true, isWritable: true });
91+
const destinationPoolAccount = parseKey(keys[i++], { isSigner: false, isWritable: true });
92+
const managerFeeAccount = parseKey(keys[i++], { isSigner: false, isWritable: true });
93+
const referralPoolAccount = parseKey(keys[i++], { isSigner: false, isWritable: true });
94+
const poolMint = parseKey(keys[i++], { isSigner: false, isWritable: true });
95+
const systemProgramProgramId = parseKey(keys[i++], { isSigner: false, isWritable: false });
96+
assert(systemProgramProgramId.equals(SystemProgram.programId), 'Unexpected pubkey in DepositSol instruction');
97+
const tokenProgramId = parseKey(keys[i++], { isSigner: false, isWritable: false });
98+
assert(tokenProgramId.equals(TOKEN_PROGRAM_ID), 'Unexpected pubkey in DepositSol instruction');
99+
const depositAuthority = keys.length > 10 ? parseKey(keys[i++], { isSigner: true, isWritable: false }) : undefined;
100+
assert(keys.length <= 11, 'Too many keys in DepositSol instruction');
101+
102+
const layoutCode = data.readUint8(0);
103+
assert(layoutCode === DEPOSIT_SOL_LAYOUT_CODE, 'Incorrect layout code in DepositSol data');
104+
assert(data.length === 9, 'Incorrect data size for DepositSol layout');
105+
const lamports = data.readBigInt64LE(1);
106+
107+
return {
108+
stakePool,
109+
depositAuthority,
110+
withdrawAuthority,
111+
reserveStake,
112+
fundingAccount,
113+
destinationPoolAccount,
114+
managerFeeAccount,
115+
referralPoolAccount,
116+
poolMint,
117+
lamports: Number(lamports),
118+
};
119+
}

modules/sdk-coin-sol/src/lib/iface.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { TransactionExplanation as BaseTransactionExplanation, Recipient } from
22
import { DecodedCloseAccountInstruction } from '@solana/spl-token';
33
import { Blockhash, StakeInstructionType, SystemInstructionType, TransactionSignature } from '@solana/web3.js';
44
import { InstructionBuilderTypes } from './constants';
5+
import { StakePoolInstructionType } from '@solana/spl-stake-pool';
56

67
// TODO(STLX-9890): Add the interfaces for validityWindow and SequenceId
78
export interface SolanaKeys {
@@ -86,6 +87,8 @@ export interface StakingActivate {
8687
amount: string;
8788
validator: string;
8889
isMarinade?: boolean;
90+
isJito?: boolean;
91+
isTestnet?: boolean;
8992
};
9093
}
9194

@@ -149,6 +152,7 @@ export interface AtaClose {
149152
export type ValidInstructionTypes =
150153
| SystemInstructionType
151154
| StakeInstructionType
155+
| StakePoolInstructionType
152156
| 'Memo'
153157
| 'InitializeAssociatedTokenAccount'
154158
| 'CloseAssociatedTokenAccount'

0 commit comments

Comments
 (0)