Skip to content

Commit 00a704a

Browse files
authored
Merge pull request #7022 from BitGo/harit/jito-add-deactivate-instruction
fix(sdk-coin-sol): add missing deactivate to jito unstaking
2 parents dbb771b + 917b985 commit 00a704a

File tree

5 files changed

+128
-14
lines changed

5 files changed

+128
-14
lines changed

examples/ts/sol/stake-jito.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ require('dotenv').config({ path: '../../.env' });
1616

1717
const AMOUNT_LAMPORTS = 1000;
1818
const JITO_STAKE_POOL_ADDRESS = 'Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb';
19-
const NETWORK = 'devnet';
19+
const NETWORK = 'testnet';
2020

2121
const bitgo = new BitGoAPI({
2222
accessToken: process.env.TESTNET_ACCESS_TOKEN,

examples/ts/sol/unstake-jito.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Unstakes JitoSOL tokens on Solana devnet.
3+
*
4+
* Copyright 2025, BitGo, Inc. All Rights Reserved.
5+
*/
6+
import { SolStakingTypeEnum } from '@bitgo/public-types';
7+
import { BitGoAPI } from '@bitgo/sdk-api';
8+
import { TransactionBuilderFactory, Tsol } from '@bitgo/sdk-coin-sol';
9+
import { coins } from '@bitgo/statics';
10+
import { Connection, PublicKey, clusterApiUrl, Keypair } from '@solana/web3.js';
11+
import { getStakePoolAccount } from '@solana/spl-stake-pool';
12+
import * as bs58 from 'bs58';
13+
14+
require('dotenv').config({ path: '../../.env' });
15+
16+
const AMOUNT_TOKENS = 100;
17+
const JITO_STAKE_POOL_ADDRESS = 'Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb';
18+
const NETWORK = 'testnet';
19+
// You must find a validator. Try prepareWithdrawAccounts.
20+
21+
const bitgo = new BitGoAPI({
22+
accessToken: process.env.TESTNET_ACCESS_TOKEN,
23+
env: 'test',
24+
});
25+
const coin = coins.get('tsol');
26+
bitgo.register(coin.name, Tsol.createInstance);
27+
28+
async function main() {
29+
const account = getAccount();
30+
const { validatorAddress } = getValidator();
31+
const connection = new Connection(clusterApiUrl(NETWORK), 'confirmed');
32+
const recentBlockhash = await connection.getLatestBlockhash();
33+
const stakePoolAddress = new PublicKey(JITO_STAKE_POOL_ADDRESS);
34+
const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
35+
console.info('Validator list account', stakePoolAccount.account.data.validatorList.toBase58());
36+
37+
const transferAuthority = Keypair.generate();
38+
const stakeAccount = Keypair.generate();
39+
console.info('Transfer authority public key:', transferAuthority.publicKey.toBase58());
40+
console.info('Stake account public key:', stakeAccount.publicKey.toBase58());
41+
42+
// Use BitGoAPI to build withdrawStake instruction
43+
const txBuilder = new TransactionBuilderFactory(coin).getStakingDeactivateBuilder();
44+
txBuilder
45+
.amount(`${AMOUNT_TOKENS}`)
46+
.sender(account.publicKey.toBase58())
47+
.stakingAddress(JITO_STAKE_POOL_ADDRESS)
48+
.unstakingAddress(stakeAccount.publicKey.toBase58())
49+
.stakingType(SolStakingTypeEnum.JITO)
50+
.extraParams({
51+
stakePoolData: {
52+
managerFeeAccount: stakePoolAccount.account.data.managerFeeAccount.toBase58(),
53+
poolMint: stakePoolAccount.account.data.poolMint.toBase58(),
54+
validatorListAccount: stakePoolAccount.account.data.validatorList.toBase58(),
55+
},
56+
validatorAddress,
57+
transferAuthorityAddress: transferAuthority.publicKey.toBase58(),
58+
})
59+
.nonce(recentBlockhash.blockhash);
60+
61+
txBuilder.sign({ key: account.secretKey });
62+
txBuilder.sign({ key: bs58.encode(stakeAccount.secretKey) });
63+
txBuilder.sign({ key: bs58.encode(transferAuthority.secretKey) });
64+
65+
const tx = await txBuilder.build();
66+
const serializedTx = tx.toBroadcastFormat();
67+
console.info(serializedTx);
68+
console.info(`Transaction JSON:\n${JSON.stringify(tx.toJson(), undefined, 2)}`);
69+
70+
// Send transaction
71+
try {
72+
const sig = await connection.sendRawTransaction(Buffer.from(serializedTx, 'base64'));
73+
await connection.confirmTransaction(sig);
74+
console.log(`${AMOUNT_TOKENS} tokens withdrawn`, sig);
75+
} catch (e) {
76+
console.log('Error sending transaction');
77+
console.error(e);
78+
}
79+
}
80+
81+
const getAccount = () => {
82+
const publicKey = process.env.ACCOUNT_PUBLIC_KEY;
83+
const secretKey = process.env.ACCOUNT_SECRET_KEY;
84+
if (publicKey === undefined || secretKey === undefined) {
85+
const { publicKey, secretKey } = Keypair.generate();
86+
console.log('# Here is a new account to save into your .env file.');
87+
console.log(`ACCOUNT_PUBLIC_KEY=${publicKey.toBase58()}`);
88+
console.log(`ACCOUNT_SECRET_KEY=${bs58.encode(secretKey)}`);
89+
throw new Error('Missing account information');
90+
}
91+
92+
return {
93+
publicKey: new PublicKey(publicKey),
94+
secretKey,
95+
secretKeyArray: new Uint8Array(bs58.decode(secretKey)),
96+
};
97+
};
98+
99+
const getValidator = () => {
100+
const validatorAddress = process.env.VALIDATOR_PUBLIC_KEY;
101+
if (validatorAddress === undefined) {
102+
console.log('# You must select a validator, then define the entry below');
103+
console.log('VALIDATOR_PUBLIC_KEY=');
104+
throw new Error('Missing validator address');
105+
}
106+
return { validatorAddress };
107+
};
108+
109+
main().catch((e) => console.error(e));

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ type JitoUnstakingInstructions = UnstakingInstructions & {
578578
};
579579

580580
function isJitoUnstakingInstructions(ui: UnstakingInstructions): ui is JitoUnstakingInstructions {
581-
return ui.withdrawStake !== undefined;
581+
return ui.withdrawStake !== undefined && ui.deactivate !== undefined;
582582
}
583583

584584
type MarinadeUnstakingInstructions = UnstakingInstructions & {
@@ -595,7 +595,7 @@ type NativeUnstakingInstructions = UnstakingInstructions & {
595595
};
596596

597597
function isNativeUnstakingInstructions(ui: UnstakingInstructions): ui is NativeUnstakingInstructions {
598-
return ui.deactivate !== undefined;
598+
return ui.withdrawStake === undefined && ui.deactivate !== undefined;
599599
}
600600

601601
function getStakingTypeFromUnstakingInstructions(ui: UnstakingInstructions): SolStakingTypeEnum {
@@ -824,7 +824,6 @@ function validateUnstakingInstructions(unstakingInstructions: UnstakingInstructi
824824
'split',
825825
'deactivate',
826826
'transfer',
827-
'withdrawStake',
828827
] as const;
829828
if (unstakingInstructionsKeys.every((k) => !!unstakingInstructions[k] === (k === 'transfer'))) {
830829
return;
@@ -841,6 +840,11 @@ function validateUnstakingInstructions(unstakingInstructions: UnstakingInstructi
841840
throw new NotSupported('Invalid deactivate stake transaction, missing deactivate stake account instruction');
842841
}
843842

843+
// This is a stake pool instruction, not a partial unstake
844+
if (unstakingInstructions.withdrawStake) {
845+
return;
846+
}
847+
844848
if (!unstakingInstructions.allocate) {
845849
throw new NotSupported(
846850
'Invalid partial deactivate stake transaction, missing allocate unstake account instruction'

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,8 @@ export type DepositSolStakePoolData = Pick<StakePoolData, 'poolMint' | 'reserveS
108108
* Construct Solana depositSol stake pool instruction from parameters.
109109
*
110110
* @param {DepositSolInstructionsParams} params - parameters for staking to stake pool
111-
* @param poolMint - pool mint derived from getStakePoolAccount
112-
* @param reserveStake - reserve account derived from getStakePoolAccount
113-
* @param managerFeeAccount - manager fee account derived from getStakePoolAccount
111+
* @param {DepositSolStakePoolData} stakePool - data from getStakePoolAccount needed for DepositSol
112+
* @param createAssociatedTokenAccount
114113
* @returns {TransactionInstruction}
115114
*/
116115
export function depositSolInstructions(
@@ -219,13 +218,11 @@ export interface WithdrawStakeInstructionsParams {
219218
export type WithdrawStakeStakePoolData = Pick<StakePoolData, 'poolMint' | 'validatorListAccount' | 'managerFeeAccount'>;
220219

221220
/**
222-
* Construct Solana depositSol stake pool instruction from parameters.
221+
* Construct Solana withdrawStake stake pool instructions from parameters.
223222
*
224-
* @param {DepositSolInstructionsParams} params - parameters for staking to stake pool
225-
* @param poolMint - pool mint derived from getStakePoolAccount
226-
* @param reserveStake - reserve account derived from getStakePoolAccount
227-
* @param managerFeeAccount - manager fee account derived from getStakePoolAccount
228-
* @returns {TransactionInstruction}
223+
* @param {WithdrawStakeInstructionsParams} params - parameters for unstaking from stake pool
224+
* @param {WithdrawStakeStakePoolData} stakePool - data from getStakePoolAccount needed for WithdrawStake
225+
* @returns {TransactionInstruction[]}
229226
*/
230227
export function withdrawStakeInstructions(
231228
params: WithdrawStakeInstructionsParams,
@@ -271,6 +268,10 @@ export function withdrawStakeInstructions(
271268
poolTokens: Number(poolAmount),
272269
withdrawAuthority,
273270
}),
271+
...StakeProgram.deactivate({
272+
stakePubkey: destinationStakeAccount,
273+
authorizedPubkey: tokenOwner,
274+
}).instructions,
274275
];
275276
}
276277

modules/sdk-coin-sol/test/resources/sol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export const MARINADE_STAKING_DEACTIVATE_SIGNED_TX =
208208
'AaiHUOzzeJaUzpdkm2BmLJI3AVOhFHTD5BnVMwUH3lRv8hKpH1fnXiXNm6ghZgNwhggXBAqhL3t4XEl+H7T95gIBAAIEReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0EL/kczKamI94jNtLT/BR9nkfa/PR2IU6d7qaEV8VVC3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUpTWpkpIQZNJOhxYNo4fHw1td28kruB5B+oQEEFRI3jMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQICAgABDAIAAABgGCMAAAAAAAMAbntcIlByZXBhcmVGb3JSZXZva2VcIjp7XCJ1c2VyXCI6XCI1aHI1ZmlzUGk2RFhOdXVScG01WFVienBpRW5tZHl4WHVCRFR3endaajVQZX1cIixcImFtb3VudFwiOlwiNTAwMDAwMDAwMDAwXCJ9';
209209

210210
export const JITO_STAKING_DEACTIVATE_SIGNED_TX =
211-
'A1+g/k5rxNY5h3qNtjw3j+t8JuhLZvzJ8+e3CSsE7PCp95lI1mj9iY/NSP0eUEBcvWpGj9CIE8CEG+EzwFcfGADpjAnLDx2pfjVqrUsJXY07JsXxCfeuSaUsoPU9JHJpROexYB/0eZUBf+dwTtiJ43vUsM7oRFdJnfMnQXnHHFoNKQ6UmtfSOXKvrYkGUYUb/Ch6fy++GtUnSR1PmNsxgs+10JXZbwM7mDIdV8jSVhKm76M+RobwxuNscbWhuCKODwMBBg9F5Xm8+SU89otH3/H7OjpcLCG6xbI8Ca4SpuBVa/CLQWJ7L7oBJ2H5uRm8+Uy7TJ3IBR2fCTKZMU05SSMyaL3p6Lfi7T4mzoclKbPedsv+JDs60KtRcBK6Y7CHyYejKikcg6pe+BOG2OETfAVS9ftz6va1oE4onLBolJ2N+ZOOhCPgdQm63e39tRapC5GXu1BHQyVdDjfF/13OiiQe7cQxl9rD5vZLBx1Aaz7SV4hmv2ZhGp4LQEU67b++EtDrIi8J5qP+7PmQMuHB32uXItyzY057jjRAk2vDSwzByOtSH/zRQemDLK8QrZF0lcoPJxtbKTzUcCfqc3AH7UDrOaC9BIo+CMO0lb4X9FQn2JvsW4DH4mlcGGTXZ0PbOb7TRtYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFTlniTAUaihSnsel5fw4Szfp0kCFmUxlaalEqbxZmArBoFO1Mr2ihdGcv2shgMaY+hOoV76HUS3IpP229sAFlAGodgXkTdUKpg0N73+KnqyVX9TXIp4citopJ3AAAAAAAan1RcYx3TJKFZjmGkdXraLXrijm0ttXHNVWyEAAAAABt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKnjMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQMOAwMCAAkE6AMAAAAAAAAJAgABNAAAAACA1SIAAAAAAMgAAAAAAAAABqHYF5E3VCqYNDe9/ip6slV/U1yKeHIraKSdwAAAAAALDQgECgUBAAIDBgcNDgwJCugDAAAAAAAA';
211+
'A7txZr55CtSJogfV1ihB1JOIuVbmhAh7BCl4hJeTBcbrq6KT+Jzbbjx4qEXDgRnMtY7cb9xnekOHUfKkW9D2RQpchl2oH4Np/+Oghy7QNjKrodZsFlqhiYoo+Zx0Bjf+Hwq35h/zVd1kHRTkaB1ebZwDeEejPrFgNCpkqxRh9ZgOMBethjkNPCrqzk50pOqx1ktJik5loScyp/81bggjQASE4jMdtET/a2jpFJeG34GZLIY6r+LNTXtGsK53qyR9CQMBBg9F5Xm8+SU89otH3/H7OjpcLCG6xbI8Ca4SpuBVa/CLQWJ7L7oBJ2H5uRm8+Uy7TJ3IBR2fCTKZMU05SSMyaL3p6Lfi7T4mzoclKbPedsv+JDs60KtRcBK6Y7CHyYejKikcg6pe+BOG2OETfAVS9ftz6va1oE4onLBolJ2N+ZOOhCPgdQm63e39tRapC5GXu1BHQyVdDjfF/13OiiQe7cQxl9rD5vZLBx1Aaz7SV4hmv2ZhGp4LQEU67b++EtDrIi8J5qP+7PmQMuHB32uXItyzY057jjRAk2vDSwzByOtSH/zRQemDLK8QrZF0lcoPJxtbKTzUcCfqc3AH7UDrOaC9BIo+CMO0lb4X9FQn2JvsW4DH4mlcGGTXZ0PbOb7TRtYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFTlniTAUaihSnsel5fw4Szfp0kCFmUxlaalEqbxZmArBoFO1Mr2ihdGcv2shgMaY+hOoV76HUS3IpP229sAFlAGodgXkTdUKpg0N73+KnqyVX9TXIp4citopJ3AAAAAAAan1RcYx3TJKFZjmGkdXraLXrijm0ttXHNVWyEAAAAABt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKnjMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQQOAwMCAAkE6AMAAAAAAAAJAgABNAAAAACA1SIAAAAAAMgAAAAAAAAABqHYF5E3VCqYNDe9/ip6slV/U1yKeHIraKSdwAAAAAALDQgECgUBAAIDBgcNDgwJCugDAAAAAAAADAMBDQAEBQAAAA==';
212212

213213
export const STAKING_DEACTIVATE_SIGNED_TX_single =
214214
'AUfyWtl4IUxhH21qX/H03hJZer1XxQaxL2r/uDTM/u1GzBIyePCHu78O2SkWGEYP6eDdiY3OLfJmUM1jiy8NCAoBAAIEReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0Fiey+6ASdh+bkZvPlMu0ydyAUdnwkymTFNOUkjMmi96Qah2BeRN1QqmDQ3vf4qerJVf1NcinhyK2ikncAAAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAADjMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQECAwEDAAQFAAAA';

0 commit comments

Comments
 (0)