Skip to content

Commit f2072d3

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

16 files changed

+585
-54
lines changed

examples/ts/sol/stake-jito.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 * as bs58 from 'bs58';
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+
33+
// Account should have sufficient balance
34+
const accountBalance = await connection.getBalance(account.publicKey)
35+
if (accountBalance < 0.1 * LAMPORTS_PER_SOL) {
36+
console.info(`Your account balance is ${accountBalance / LAMPORTS_PER_SOL} SOL, requesting airdrop`)
37+
const sig = await connection.requestAirdrop(account.publicKey, 2 * LAMPORTS_PER_SOL)
38+
await connection.confirmTransaction(sig)
39+
console.info(`Airdrop successful: ${sig}`)
40+
}
41+
42+
// Stake pool should be up to date
43+
const epochInfo = await connection.getEpochInfo()
44+
if (stakePoolAccount.account.data.lastUpdateEpoch.ltn(epochInfo.epoch)) {
45+
console.info('Stake pool is out of date.')
46+
const usp = await updateStakePool(connection, stakePoolAccount)
47+
const tx = new Transaction()
48+
tx.add(...usp.updateListInstructions, ...usp.finalInstructions)
49+
const signer = Keypair.fromSecretKey(account.secretKeyArray)
50+
const sig = await connection.sendTransaction(tx, [signer])
51+
await connection.confirmTransaction(sig)
52+
console.info(`Stake pool updated: ${sig}`)
53+
}
54+
55+
// Use BitGoAPI to build depositSol instruction
56+
const txBuilder = new TransactionBuilderFactory(coin).getStakingActivateBuilder()
57+
txBuilder
58+
.amount(`${AMOUNT_LAMPORTS}`)
59+
.sender(account.publicKey.toBase58())
60+
.stakingAddress(JITO_STAKE_POOL_ADDRESS)
61+
.validator(JITO_STAKE_POOL_ADDRESS)
62+
.isJito(true)
63+
.nonce(recentBlockhash.blockhash)
64+
txBuilder.sign({ key: account.secretKey })
65+
const tx = await txBuilder.build()
66+
const serializedTx = tx.toBroadcastFormat()
67+
console.info(`Transaction JSON:\n${JSON.stringify(tx.toJson(), undefined, 2)}`)
68+
69+
// Send transaction
70+
try {
71+
const sig = await connection.sendRawTransaction(Buffer.from(serializedTx, 'base64'))
72+
await connection.confirmTransaction(sig)
73+
console.log(`${AMOUNT_LAMPORTS / LAMPORTS_PER_SOL} SOL deposited`, sig)
74+
} catch (e) {
75+
console.log('Error sending transaction')
76+
console.error(e)
77+
if (e.transactionMessage === 'Transaction simulation failed: Error processing Instruction 0: Provided owner is not allowed') {
78+
console.error('If you successfully staked JitoSOL once, you cannot stake again.')
79+
}
80+
}
81+
}
82+
83+
const getAccount = () => {
84+
const publicKey = process.env.ACCOUNT_PUBLIC_KEY
85+
const secretKey = process.env.ACCOUNT_SECRET_KEY
86+
if (publicKey === undefined || secretKey === 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=${bs58.encode(secretKey)}`)
91+
throw new Error("Missing account information")
92+
}
93+
94+
return {
95+
publicKey: new PublicKey(publicKey),
96+
secretKey,
97+
secretKeyArray: new Uint8Array(bs58.decode(secretKey)),
98+
}
99+
}
100+
101+
main().catch((e) => console.error(e))

modules/sdk-coin-sol/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@bitgo/sdk-core": "^36.0.0",
4444
"@bitgo/sdk-lib-mpc": "^10.6.0",
4545
"@bitgo/statics": "^57.0.0",
46+
"@solana/spl-stake-pool": "1.1.8",
4647
"@solana/spl-token": "0.3.1",
4748
"@solana/web3.js": "1.92.1",
4849
"bignumber.js": "^9.0.0",

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',
@@ -30,6 +37,7 @@ export enum ValidInstructionTypesEnum {
3037
SetPriorityFee = 'SetPriorityFee',
3138
MintTo = 'MintTo',
3239
Burn = 'Burn',
40+
DepositSol = 'DepositSol',
3341
}
3442

3543
// Internal instructions types
@@ -72,6 +80,7 @@ export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [
7280
ValidInstructionTypesEnum.SetPriorityFee,
7381
ValidInstructionTypesEnum.MintTo,
7482
ValidInstructionTypesEnum.Burn,
83+
ValidInstructionTypesEnum.DepositSol,
7584
];
7685

7786
/** Const to check the order of the Wallet Init instructions when decode */
@@ -96,6 +105,12 @@ export const marinadeStakingActivateInstructionsIndexes = {
96105
Memo: 2,
97106
} as const;
98107

108+
/** Const to check the order of the Jito Staking Activate instructions when decode */
109+
export const jitoStakingActivateInstructionsIndexes = {
110+
AtaInit: 0,
111+
DepositSol: 1,
112+
} as const;
113+
99114
/** Const to check the order of the Staking Authorize instructions when decode */
100115
export const stakingAuthorizeInstructionsIndexes = {
101116
Authorize: 0,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
TransactionSignature,
99
} from '@solana/web3.js';
1010
import { InstructionBuilderTypes } from './constants';
11+
import { StakePoolInstructionType } from '@solana/spl-stake-pool';
1112

1213
// TODO(STLX-9890): Add the interfaces for validityWindow and SequenceId
1314
export interface SolanaKeys {
@@ -121,6 +122,7 @@ export interface StakingActivate {
121122
amount: string;
122123
validator: string;
123124
isMarinade?: boolean;
125+
isJito?: boolean;
124126
};
125127
}
126128

@@ -184,6 +186,7 @@ export interface AtaClose {
184186
export type ValidInstructionTypes =
185187
| SystemInstructionType
186188
| StakeInstructionType
189+
| StakePoolInstructionType
187190
| 'Memo'
188191
| 'InitializeAssociatedTokenAccount'
189192
| 'CloseAssociatedTokenAccount'

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

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ import {
2727
import { NotSupported, TransactionType } from '@bitgo/sdk-core';
2828
import { coins, SolCoin } from '@bitgo/statics';
2929
import assert from 'assert';
30-
import { InstructionBuilderTypes, ValidInstructionTypesEnum, walletInitInstructionIndexes } from './constants';
30+
import {
31+
InstructionBuilderTypes,
32+
JITO_STAKE_POOL_ADDRESS,
33+
ValidInstructionTypesEnum,
34+
walletInitInstructionIndexes,
35+
} from './constants';
3136
import {
3237
AtaClose,
3338
AtaInit,
@@ -47,6 +52,8 @@ import {
4752
SetPriorityFee,
4853
} from './iface';
4954
import { getInstructionType } from './utils';
55+
import { DepositSolParams } from '@solana/spl-stake-pool';
56+
import { decodeDepositSol } from './jitoStakePoolOperations';
5057

5158
/**
5259
* Construct instructions params from Solana instructions
@@ -303,6 +310,14 @@ function parseSendInstructions(
303310
return instructionData;
304311
}
305312

313+
function stakingInstructionsIsMarinade(si: StakingInstructions): boolean {
314+
return !!(si.delegate === undefined && si.depositSol === undefined);
315+
}
316+
317+
function stakingInstructionsIsJito(si: StakingInstructions): boolean {
318+
return !!(si.delegate === undefined && si.depositSol?.stakePool.toString() === JITO_STAKE_POOL_ADDRESS);
319+
}
320+
306321
/**
307322
* Parses Solana instructions to create staking tx and delegate tx instructions params
308323
* Only supports Nonce, StakingActivate and Memo Solana instructions
@@ -312,8 +327,8 @@ function parseSendInstructions(
312327
*/
313328
function parseStakingActivateInstructions(
314329
instructions: TransactionInstruction[]
315-
): Array<Nonce | StakingActivate | Memo> {
316-
const instructionData: Array<Nonce | StakingActivate | Memo> = [];
330+
): Array<Nonce | StakingActivate | Memo | AtaInit> {
331+
const instructionData: Array<Nonce | StakingActivate | Memo | AtaInit> = [];
317332
const stakingInstructions = {} as StakingInstructions;
318333
for (const instruction of instructions) {
319334
const type = getInstructionType(instruction);
@@ -346,21 +361,48 @@ function parseStakingActivateInstructions(
346361
case ValidInstructionTypesEnum.StakingDelegate:
347362
stakingInstructions.delegate = StakeInstruction.decodeDelegate(instruction);
348363
break;
364+
365+
case ValidInstructionTypesEnum.DepositSol:
366+
stakingInstructions.depositSol = decodeDepositSol(instruction);
367+
break;
368+
369+
case ValidInstructionTypesEnum.InitializeAssociatedTokenAccount:
370+
instructionData.push({
371+
type: InstructionBuilderTypes.CreateAssociatedTokenAccount,
372+
params: {
373+
mintAddress: instruction.keys[ataInitInstructionKeysIndexes.MintAddress].pubkey.toString(),
374+
ataAddress: instruction.keys[ataInitInstructionKeysIndexes.ATAAddress].pubkey.toString(),
375+
ownerAddress: instruction.keys[ataInitInstructionKeysIndexes.OwnerAddress].pubkey.toString(),
376+
payerAddress: instruction.keys[ataInitInstructionKeysIndexes.PayerAddress].pubkey.toString(),
377+
tokenName: findTokenName(instruction.keys[ataInitInstructionKeysIndexes.MintAddress].pubkey.toString()),
378+
},
379+
});
380+
break;
349381
}
350382
}
351383

352384
validateStakingInstructions(stakingInstructions);
385+
353386
const stakingActivate: StakingActivate = {
354387
type: InstructionBuilderTypes.StakingActivate,
355388
params: {
356-
fromAddress: stakingInstructions.create?.fromPubkey.toString() || '',
357-
stakingAddress: stakingInstructions.initialize?.stakePubkey.toString() || '',
358-
amount: stakingInstructions.create?.lamports.toString() || '',
389+
fromAddress:
390+
stakingInstructions.create?.fromPubkey.toString() ||
391+
stakingInstructions.depositSol?.fundingAccount.toString() ||
392+
'',
393+
stakingAddress:
394+
stakingInstructions.initialize?.stakePubkey.toString() ||
395+
stakingInstructions.depositSol?.stakePool.toString() ||
396+
'',
397+
amount:
398+
stakingInstructions.create?.lamports.toString() || stakingInstructions.depositSol?.lamports.toString() || '',
359399
validator:
360400
stakingInstructions.delegate?.votePubkey.toString() ||
361401
stakingInstructions.initialize?.authorized.staker.toString() ||
402+
stakingInstructions.depositSol?.stakePool.toString() ||
362403
'',
363-
isMarinade: stakingInstructions.delegate === undefined,
404+
isMarinade: stakingInstructionsIsMarinade(stakingInstructions),
405+
isJito: stakingInstructionsIsJito(stakingInstructions),
364406
},
365407
};
366408
instructionData.push(stakingActivate);
@@ -413,22 +455,23 @@ interface StakingInstructions {
413455
initialize?: InitializeStakeParams;
414456
delegate?: DelegateStakeParams;
415457
authorize?: AuthorizeStakeParams[];
458+
depositSol?: DepositSolParams;
416459
}
417460

418461
function validateStakingInstructions(stakingInstructions: StakingInstructions) {
419-
if (!stakingInstructions.create) {
420-
throw new NotSupported('Invalid staking activate transaction, missing create stake account instruction');
421-
}
422-
423-
if (!stakingInstructions.initialize && stakingInstructions.delegate) {
424-
return;
425-
} else if (!stakingInstructions.delegate && stakingInstructions.initialize) {
426-
return;
427-
} else if (!stakingInstructions.delegate && !stakingInstructions.initialize) {
428-
// If both are missing something is wrong
429-
throw new NotSupported(
430-
'Invalid staking activate transaction, missing initialize stake account/delegate instruction'
431-
);
462+
if (stakingInstructionsIsJito(stakingInstructions)) {
463+
if (!stakingInstructions.depositSol) {
464+
throw new NotSupported('Invalid staking activate transaction, missing deposit sol instruction');
465+
}
466+
} else {
467+
if (!stakingInstructions.create) {
468+
throw new NotSupported('Invalid staking activate transaction, missing create stake account instruction');
469+
}
470+
if (!stakingInstructions.delegate && !stakingInstructions.initialize) {
471+
throw new NotSupported(
472+
'Invalid staking activate transaction, missing initialize stake account/delegate instruction'
473+
);
474+
}
432475
}
433476
}
434477

@@ -776,6 +819,9 @@ function parseAtaInitInstructions(
776819
};
777820
instructionData.push(ataInit);
778821
break;
822+
case ValidInstructionTypesEnum.DepositSol:
823+
// AtaInit is a part of spl-stake-pool's depositSol process
824+
break;
779825
default:
780826
throw new NotSupported(
781827
'Invalid transaction, instruction type not supported: ' + getInstructionType(instruction)

0 commit comments

Comments
 (0)