From bb9af1ea4678f2af336cf1e458a2b2b9e026a225 Mon Sep 17 00:00:00 2001 From: Harit Kapadia Date: Wed, 6 Aug 2025 13:30:31 -0400 Subject: [PATCH 1/2] feat(sdk-coin-sol): implements staking deactivate for jito Ticket: SC-2418 --- examples/ts/sol/stake-jito.ts | 9 +- modules/sdk-coin-sol/src/lib/constants.ts | 13 + modules/sdk-coin-sol/src/lib/iface.ts | 25 +- .../src/lib/instructionParamsFactory.ts | 204 ++++++++++------ .../src/lib/jitoStakePoolOperations.ts | 226 +++++++++++++++++- .../src/lib/solInstructionFactory.ts | 100 +++++--- .../src/lib/stakingActivateBuilder.ts | 14 ++ .../src/lib/stakingDeactivateBuilder.ts | 62 ++++- .../src/lib/transactionBuilder.ts | 6 +- modules/sdk-coin-sol/src/lib/utils.ts | 68 ++---- modules/sdk-coin-sol/test/resources/sol.ts | 16 +- .../test/unit/solInstructionFactory.ts | 43 ++-- .../stakingActivateBuilder.ts | 14 ++ .../stakingDeactivateBuilder.ts | 58 +++++ 14 files changed, 652 insertions(+), 206 deletions(-) diff --git a/examples/ts/sol/stake-jito.ts b/examples/ts/sol/stake-jito.ts index 95c60af330..c9aebc9e77 100644 --- a/examples/ts/sol/stake-jito.ts +++ b/examples/ts/sol/stake-jito.ts @@ -60,6 +60,13 @@ async function main() { .stakingAddress(JITO_STAKE_POOL_ADDRESS) .validator(JITO_STAKE_POOL_ADDRESS) .isJito(true) + .jitoParams({ + stakePoolData: { + managerFeeAccount: stakePoolAccount.account.data.managerFeeAccount.toString(), + poolMint: stakePoolAccount.account.data.poolMint.toString(), + reserveStake: stakePoolAccount.account.data.toString(), + } + }) .nonce(recentBlockhash.blockhash) txBuilder.sign({ key: account.secretKey }) const tx = await txBuilder.build() @@ -85,7 +92,7 @@ const getAccount = () => { const secretKey = process.env.ACCOUNT_SECRET_KEY if (publicKey === undefined || secretKey === undefined) { const { publicKey, secretKey } = Keypair.generate() - console.log('Here is a new account to save into your .env file.') + 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=${bs58.encode(secretKey)}`) throw new Error("Missing account information") diff --git a/modules/sdk-coin-sol/src/lib/constants.ts b/modules/sdk-coin-sol/src/lib/constants.ts index 2a92afdf4c..69848ae7ca 100644 --- a/modules/sdk-coin-sol/src/lib/constants.ts +++ b/modules/sdk-coin-sol/src/lib/constants.ts @@ -38,6 +38,8 @@ export enum ValidInstructionTypesEnum { MintTo = 'MintTo', Burn = 'Burn', DepositSol = 'DepositSol', + WithdrawStake = 'WithdrawStake', + Approve = 'Approve', } // Internal instructions types @@ -58,6 +60,8 @@ export enum InstructionBuilderTypes { MintTo = 'MintTo', Burn = 'Burn', CustomInstruction = 'CustomInstruction', + Approve = 'Approve', + WithdrawStake = 'WithdrawStake', } export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [ @@ -80,7 +84,9 @@ export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [ ValidInstructionTypesEnum.SetPriorityFee, ValidInstructionTypesEnum.MintTo, ValidInstructionTypesEnum.Burn, + ValidInstructionTypesEnum.Approve, ValidInstructionTypesEnum.DepositSol, + ValidInstructionTypesEnum.WithdrawStake, ]; /** Const to check the order of the Wallet Init instructions when decode */ @@ -111,6 +117,13 @@ export const jitoStakingActivateInstructionsIndexes = { DepositSol: 1, } as const; +/** Const to check the order of the Jito Staking Activate instructions when decode */ +export const jitoStakingDeactivateInstructionsIndexes = { + Approve: 0, + Create: 1, + WithdrawStake: 2, +} as const; + /** Const to check the order of the Staking Authorize instructions when decode */ export const stakingAuthorizeInstructionsIndexes = { Authorize: 0, diff --git a/modules/sdk-coin-sol/src/lib/iface.ts b/modules/sdk-coin-sol/src/lib/iface.ts index 6a4e28fbe1..60cf3dc0b7 100644 --- a/modules/sdk-coin-sol/src/lib/iface.ts +++ b/modules/sdk-coin-sol/src/lib/iface.ts @@ -3,6 +3,7 @@ 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'; +import { DepositSolStakePoolData, WithdrawStakeStakePoolData } from './jitoStakePoolOperations'; // TODO(STLX-9890): Add the interfaces for validityWindow and SequenceId export interface SolanaKeys { @@ -41,6 +42,7 @@ export type InstructionParams = | StakingDelegate | MintTo | Burn + | Approve | CustomInstruction; export interface Memo { @@ -107,6 +109,17 @@ export interface Burn { }; } +export interface Approve { + type: InstructionBuilderTypes.Approve; + params: { + accountAddress: string; + delegateAddress: string; + ownerAddress: string; + amount: string; + programId?: string; + }; +} + export interface StakingActivate { type: InstructionBuilderTypes.StakingActivate; params: { @@ -116,6 +129,9 @@ export interface StakingActivate { validator: string; isMarinade?: boolean; isJito?: boolean; + jitoParams?: { + stakePoolData: DepositSolStakePoolData; + }; }; } @@ -132,6 +148,12 @@ export interface StakingDeactivate { amount?: string; unstakingAddress?: string; isMarinade?: boolean; + isJito?: boolean; + jitoParams?: { + stakePoolData: WithdrawStakeStakePoolData; + validatorAddress: string; + transferAuthorityAddress: string; + }; recipients?: Recipient[]; }; } @@ -187,7 +209,8 @@ export type ValidInstructionTypes = | 'TokenTransfer' | 'SetPriorityFee' | 'MintTo' - | 'Burn'; + | 'Burn' + | 'Approve'; export type StakingAuthorizeParams = { stakingAddress: string; diff --git a/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts b/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts index de466fec3d..97b00b9a1c 100644 --- a/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts +++ b/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts @@ -6,6 +6,7 @@ import { DecodedMintToInstruction, decodeMintToInstruction, TOKEN_2022_PROGRAM_ID, + decodeApproveInstruction, } from '@solana/spl-token'; import { AllocateParams, @@ -51,10 +52,11 @@ import { WalletInit, SetPriorityFee, CustomInstruction, + Approve, } from './iface'; import { getInstructionType } from './utils'; -import { DepositSolParams } from '@solana/spl-stake-pool'; -import { decodeDepositSol } from './jitoStakePoolOperations'; +import { DepositSolParams, WithdrawStakeParams } from '@solana/spl-stake-pool'; +import { decodeDepositSol, decodeWithdrawStake } from './jitoStakePoolOperations'; /** * Construct instructions params from Solana instructions @@ -141,9 +143,9 @@ function parseSendInstructions( instructions: TransactionInstruction[], instructionMetadata?: InstructionParams[], _useTokenAddressTokenName?: boolean -): Array { +): Array { const instructionData: Array< - Nonce | Memo | Transfer | TokenTransfer | AtaInit | AtaClose | SetPriorityFee | MintTo | Burn + Nonce | Memo | Transfer | TokenTransfer | AtaInit | AtaClose | SetPriorityFee | MintTo | Burn | Approve > = []; for (const instruction of instructions) { const type = getInstructionType(instruction); @@ -203,6 +205,21 @@ function parseSendInstructions( }; instructionData.push(tokenTransfer); break; + case ValidInstructionTypesEnum.Approve: + const programId = instruction.programId.equals(TOKEN_2022_PROGRAM_ID) ? TOKEN_2022_PROGRAM_ID : undefined; + const approveInstruction = decodeApproveInstruction(instruction, programId); + const approve: Approve = { + type: InstructionBuilderTypes.Approve, + params: { + accountAddress: approveInstruction.keys.account.toString(), + delegateAddress: approveInstruction.keys.delegate.toString(), + ownerAddress: approveInstruction.keys.owner.toString(), + amount: approveInstruction.data.amount.toString(), + programId: programId && programId.toString(), + }, + }; + instructionData.push(approve); + break; case ValidInstructionTypesEnum.InitializeAssociatedTokenAccount: const mintAddress = instruction.keys[ataInitInstructionKeysIndexes.MintAddress].pubkey.toString(); const mintTokenName = findTokenName(mintAddress, instructionMetadata, _useTokenAddressTokenName); @@ -406,6 +423,17 @@ function parseStakingActivateInstructions( '', isMarinade: stakingInstructionsIsMarinade(stakingInstructions), isJito: stakingInstructionsIsJito(stakingInstructions), + ...(stakingInstructions.depositSol && stakingInstructionsIsJito(stakingInstructions) + ? { + jitoParams: { + stakePoolData: { + managerFeeAccount: stakingInstructions.depositSol.managerFeeAccount.toString(), + poolMint: stakingInstructions.depositSol.poolMint.toString(), + reserveStake: stakingInstructions.depositSol.reserveStake.toString(), + }, + }, + } + : {}), }, }; instructionData.push(stakingActivate); @@ -588,31 +616,66 @@ function parseStakingDeactivateInstructions( }); } break; + + case ValidInstructionTypesEnum.WithdrawStake: + if ( + unstakingInstructions.length > 0 && + unstakingInstructions[unstakingInstructions.length - 1].withdrawStake === undefined + ) { + unstakingInstructions[unstakingInstructions.length - 1].withdrawStake = decodeWithdrawStake(instruction); + } else { + unstakingInstructions.push({ + withdrawStake: decodeWithdrawStake(instruction), + }); + } + break; } } for (const unstakingInstruction of unstakingInstructions) { validateUnstakingInstructions(unstakingInstruction); + const isMarinade = + unstakingInstruction.deactivate === undefined && unstakingInstruction.withdrawStake === undefined; const stakingDeactivate: StakingDeactivate = { type: InstructionBuilderTypes.StakingDeactivate, params: { - fromAddress: unstakingInstruction.deactivate?.authorizedPubkey.toString() || '', + fromAddress: + unstakingInstruction.deactivate?.authorizedPubkey.toString() || + unstakingInstruction.withdrawStake?.destinationStakeAuthority.toString() || + '', stakingAddress: unstakingInstruction.split?.stakePubkey.toString() || unstakingInstruction.deactivate?.stakePubkey.toString() || + unstakingInstruction.withdrawStake?.stakePool.toString() || '', - amount: unstakingInstruction.split?.lamports.toString(), - unstakingAddress: unstakingInstruction.split?.splitStakePubkey.toString(), - isMarinade: unstakingInstruction.deactivate === undefined, - recipients: - unstakingInstruction.deactivate === undefined - ? [ - { - address: unstakingInstruction.transfer?.toPubkey.toString() || '', - amount: unstakingInstruction.transfer?.lamports.toString() || '', + amount: + unstakingInstruction.split?.lamports.toString() || unstakingInstruction.withdrawStake?.poolTokens.toString(), + unstakingAddress: + unstakingInstruction.split?.splitStakePubkey.toString() || + unstakingInstruction.withdrawStake?.destinationStake.toString(), + isMarinade: isMarinade, + recipients: isMarinade + ? [ + { + address: unstakingInstruction.transfer?.toPubkey.toString() || '', + amount: unstakingInstruction.transfer?.lamports.toString() || '', + }, + ] + : undefined, + ...(unstakingInstruction.withdrawStake !== undefined + ? { + isJito: unstakingInstruction.withdrawStake !== undefined, + jitoParams: unstakingInstruction.withdrawStake && { + stakePoolData: { + managerFeeAccount: unstakingInstruction.withdrawStake.managerFeeAccount.toString(), + poolMint: unstakingInstruction.withdrawStake.poolMint.toString(), + validatorList: unstakingInstruction.withdrawStake.validatorList.toString(), }, - ] - : undefined, + validatorAddress: unstakingInstruction.withdrawStake.validatorStake.toString(), + transferAuthorityAddress: unstakingInstruction.withdrawStake.sourceTransferAuthority.toString(), + }, + } + : {}), }, }; instructionData.push(stakingDeactivate); @@ -627,65 +690,70 @@ interface UnstakingInstructions { split?: SplitStakeParams; deactivate?: DeactivateStakeParams; transfer?: DecodedTransferInstruction; + withdrawStake?: WithdrawStakeParams; } function validateUnstakingInstructions(unstakingInstructions: UnstakingInstructions) { + // Cases where exactly one field should be present + const unstakingInstructionsKeys: (keyof UnstakingInstructions)[] = [ + 'allocate', + 'assign', + 'split', + 'deactivate', + 'transfer', + 'withdrawStake', + ] as const; + if (unstakingInstructionsKeys.every((k) => !!unstakingInstructions[k] === (k === 'transfer'))) { + return; + } + if (unstakingInstructionsKeys.every((k) => !!unstakingInstructions[k] === (k === 'withdrawStake'))) { + return; + } + if (unstakingInstructionsKeys.every((k) => !!unstakingInstructions[k] === (k === 'deactivate'))) { + return; + } + + // Cases where deactivate field must be present with another field if (!unstakingInstructions.deactivate) { - if ( - unstakingInstructions.transfer && - !unstakingInstructions.allocate && - !unstakingInstructions.assign && - !unstakingInstructions.split - ) { - return; - } throw new NotSupported('Invalid deactivate stake transaction, missing deactivate stake account instruction'); + } + + if (!unstakingInstructions.allocate) { + throw new NotSupported( + 'Invalid partial deactivate stake transaction, missing allocate unstake account instruction' + ); + } else if (!unstakingInstructions.assign) { + throw new NotSupported('Invalid partial deactivate stake transaction, missing assign unstake account instruction'); + } else if (!unstakingInstructions.split) { + throw new NotSupported('Invalid partial deactivate stake transaction, missing split stake account instruction'); } else if ( - unstakingInstructions.allocate || - unstakingInstructions.assign || - unstakingInstructions.split || - unstakingInstructions.transfer + unstakingInstructions.allocate.accountPubkey.toString() !== unstakingInstructions.assign.accountPubkey.toString() ) { - if (!unstakingInstructions.allocate) { - throw new NotSupported( - 'Invalid partial deactivate stake transaction, missing allocate unstake account instruction' - ); - } else if (!unstakingInstructions.assign) { - throw new NotSupported( - 'Invalid partial deactivate stake transaction, missing assign unstake account instruction' - ); - } else if (!unstakingInstructions.split) { - throw new NotSupported('Invalid partial deactivate stake transaction, missing split stake account instruction'); - } else if ( - unstakingInstructions.allocate.accountPubkey.toString() !== unstakingInstructions.assign.accountPubkey.toString() - ) { - throw new NotSupported( - 'Invalid partial deactivate stake transaction, must allocate and assign the same public key' - ); - } else if (unstakingInstructions.allocate.space !== StakeProgram.space) { - throw new NotSupported( - `Invalid partial deactivate stake transaction, unstaking account must allocate ${StakeProgram.space} bytes` - ); - } else if (unstakingInstructions.assign.programId.toString() !== StakeProgram.programId.toString()) { - throw new NotSupported( - 'Invalid partial deactivate stake transaction, the unstake account must be assigned to the Stake Program' - ); - } else if ( - unstakingInstructions.allocate.accountPubkey.toString() !== - unstakingInstructions.split.splitStakePubkey.toString() - ) { - throw new NotSupported('Invalid partial deactivate stake transaction, must allocate the unstaking account'); - } else if ( - unstakingInstructions.split.stakePubkey.toString() === unstakingInstructions.split.splitStakePubkey.toString() - ) { - throw new NotSupported( - 'Invalid partial deactivate stake transaction, the unstaking account must be different from the Stake Account' - ); - } else if (!unstakingInstructions.transfer) { - throw new NotSupported( - 'Invalid partial deactivate stake transaction, missing funding of unstake address instruction' - ); - } + throw new NotSupported( + 'Invalid partial deactivate stake transaction, must allocate and assign the same public key' + ); + } else if (unstakingInstructions.allocate.space !== StakeProgram.space) { + throw new NotSupported( + `Invalid partial deactivate stake transaction, unstaking account must allocate ${StakeProgram.space} bytes` + ); + } else if (unstakingInstructions.assign.programId.toString() !== StakeProgram.programId.toString()) { + throw new NotSupported( + 'Invalid partial deactivate stake transaction, the unstake account must be assigned to the Stake Program' + ); + } else if ( + unstakingInstructions.allocate.accountPubkey.toString() !== unstakingInstructions.split.splitStakePubkey.toString() + ) { + throw new NotSupported('Invalid partial deactivate stake transaction, must allocate the unstaking account'); + } else if ( + unstakingInstructions.split.stakePubkey.toString() === unstakingInstructions.split.splitStakePubkey.toString() + ) { + throw new NotSupported( + 'Invalid partial deactivate stake transaction, the unstaking account must be different from the Stake Account' + ); + } else if (!unstakingInstructions.transfer) { + throw new NotSupported( + 'Invalid partial deactivate stake transaction, missing funding of unstake address instruction' + ); } } diff --git a/modules/sdk-coin-sol/src/lib/jitoStakePoolOperations.ts b/modules/sdk-coin-sol/src/lib/jitoStakePoolOperations.ts index e053e224a6..9b09d3a3db 100644 --- a/modules/sdk-coin-sol/src/lib/jitoStakePoolOperations.ts +++ b/modules/sdk-coin-sol/src/lib/jitoStakePoolOperations.ts @@ -3,16 +3,98 @@ * '@solana/spl-token', this module may no longer be necessary. */ -import { StakePoolInstruction, STAKE_POOL_PROGRAM_ID, DepositSolParams } from '@solana/spl-stake-pool'; import { + StakePoolInstruction, + STAKE_POOL_PROGRAM_ID, + DepositSolParams, + WithdrawStakeParams, +} from '@solana/spl-stake-pool'; +import { + createApproveInstruction, createAssociatedTokenAccountInstruction, getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID, } from '@solana/spl-token'; -import { AccountMeta, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js'; +import { + AccountMeta, + PublicKey, + StakeProgram, + SystemProgram, + SYSVAR_CLOCK_PUBKEY, + TransactionInstruction, +} from '@solana/web3.js'; import assert from 'assert'; +import { STAKE_ACCOUNT_RENT_EXEMPT_AMOUNT } from './constants'; + +/** + * Replicates the fields in @solana/spl-stake-pool Fee. + * + * @see {Fee} + */ +export interface Fee { + denominator: string; + numerator: string; +} + +/** + * Replicates the fields in @solana/spl-stake-pool StakePoolLayout. + * + * @see {StakePoolLayout} + */ +export interface StakePoolData { + accountType: number; + manager: string; + staker: string; + stakeDepositAuthority: string; + stakeWithdrawBumpSeed: number; + validatorList: string; + reserveStake: string; + poolMint: string; + managerFeeAccount: string; + tokenProgramId: string; + totalLamports: string; + poolTokenSupply: string; + lastUpdateEpoch: string; + lockup: { + unixTimestamp: string; + epoch: string; + custodian: string; + }; + epochFee: Fee; + nextEpochFee?: Fee | undefined; + preferredDepositValidatorVoteAddress?: string | undefined; + preferredWithdrawValidatorVoteAddress?: string | undefined; + stakeDepositFee: Fee; + stakeWithdrawalFee: Fee; + nextStakeWithdrawalFee?: Fee | undefined; + stakeReferralFee: number; + solDepositAuthority?: string | undefined; + solDepositFee: Fee; + solReferralFee: number; + solWithdrawAuthority?: string | undefined; + solWithdrawalFee: Fee; + nextSolWithdrawalFee?: Fee | undefined; + lastEpochPoolTokenSupply: string; + lastEpochTotalLamports: string; +} export const DEPOSIT_SOL_LAYOUT_CODE = 14; +export const WITHDRAW_STAKE_LAYOUT_CODE = 10; + +/** + * Generates the withdraw authority program address for the stake pool. + * Like findWithdrawAuthorityProgramAddress in @solana/spl-stake-pool, + * but synchronous. + * + * @see {findWithdrawAuthorityProgramAddress} + */ +export function findWithdrawAuthorityProgramAddressSync(programId: PublicKey, stakePoolAddress: PublicKey): PublicKey { + const [withdrawAuthority] = PublicKey.findProgramAddressSync( + [stakePoolAddress.toBuffer(), Buffer.from('withdraw')], + programId + ); + return withdrawAuthority; +} export interface DepositSolInstructionsParams { stakePoolAddress: PublicKey; @@ -20,6 +102,8 @@ export interface DepositSolInstructionsParams { lamports: bigint; } +export type DepositSolStakePoolData = Pick; + /** * Construct Solana depositSol stake pool instruction from parameters. * @@ -31,17 +115,15 @@ export interface DepositSolInstructionsParams { */ export function depositSolInstructions( params: DepositSolInstructionsParams, - poolMint: PublicKey, - reserveStake: PublicKey, - managerFeeAccount: PublicKey + stakePool: DepositSolStakePoolData ): TransactionInstruction[] { const { stakePoolAddress, from, lamports } = params; + const poolMint = new PublicKey(stakePool.poolMint); + const reserveStake = new PublicKey(stakePool.reserveStake); + const managerFeeAccount = new PublicKey(stakePool.managerFeeAccount); // findWithdrawAuthorityProgramAddress - const [withdrawAuthority] = PublicKey.findProgramAddressSync( - [stakePoolAddress.toBuffer(), Buffer.from('withdraw')], - STAKE_POOL_PROGRAM_ID - ); + const withdrawAuthority = findWithdrawAuthorityProgramAddressSync(STAKE_POOL_PROGRAM_ID, stakePoolAddress); const associatedAddress = getAssociatedTokenAddressSync(poolMint, from); @@ -52,9 +134,9 @@ export function depositSolInstructions( reserveStake, fundingAccount: from, destinationPoolAccount: associatedAddress, - managerFeeAccount: managerFeeAccount, + managerFeeAccount, referralPoolAccount: associatedAddress, - poolMint: poolMint, + poolMint, lamports: Number(lamports), withdrawAuthority, }), @@ -64,7 +146,7 @@ export function depositSolInstructions( 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' + `Unexpected key metadata in instruction: { isSigner: ${key.isSigner}, isWritable: ${key.isWritable} }` ); return key.pubkey; } @@ -117,3 +199,123 @@ export function decodeDepositSol(instruction: TransactionInstruction): DepositSo lamports: Number(lamports), }; } + +export interface WithdrawStakeInstructionsParams { + stakePoolAddress: PublicKey; + tokenOwner: PublicKey; + destinationStakeAccount: PublicKey; + validatorAddress: PublicKey; + transferAuthority: PublicKey; + poolAmount: string; +} + +export type WithdrawStakeStakePoolData = Pick; + +/** + * 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 withdrawStakeInstructions( + params: WithdrawStakeInstructionsParams, + stakePool: WithdrawStakeStakePoolData +): TransactionInstruction[] { + const { + tokenOwner, + stakePoolAddress, + destinationStakeAccount, + validatorAddress, + transferAuthority, + poolAmount: poolAmountString, + } = params; + + const poolMint = new PublicKey(stakePool.poolMint); + const validatorList = new PublicKey(stakePool.validatorList); + const managerFeeAccount = new PublicKey(stakePool.managerFeeAccount); + + const poolTokenAccount = getAssociatedTokenAddressSync(poolMint, tokenOwner); + const withdrawAuthority = findWithdrawAuthorityProgramAddressSync(STAKE_POOL_PROGRAM_ID, stakePoolAddress); + + const poolAmount = BigInt(poolAmountString); + + return [ + createApproveInstruction(poolTokenAccount, tokenOwner, tokenOwner, poolAmount), + SystemProgram.createAccount({ + fromPubkey: tokenOwner, + newAccountPubkey: destinationStakeAccount, + lamports: STAKE_ACCOUNT_RENT_EXEMPT_AMOUNT, + space: StakeProgram.space, + programId: StakeProgram.programId, + }), + StakePoolInstruction.withdrawStake({ + stakePool: stakePoolAddress, + validatorList: validatorList, + validatorStake: validatorAddress, + destinationStake: destinationStakeAccount, + destinationStakeAuthority: tokenOwner, + sourceTransferAuthority: transferAuthority, + sourcePoolAccount: poolTokenAccount, + managerFeeAccount: managerFeeAccount, + poolMint: poolMint, + poolTokens: Number(poolAmount), + withdrawAuthority, + }), + ]; +} + +/** + * Construct Solana withdrawStake stake pool parameters from instruction. + * + * @param {TransactionInstruction} instruction + * @returns {DepositSolParams} + */ +export function decodeWithdrawStake(instruction: TransactionInstruction): WithdrawStakeParams { + const { programId, keys, data } = instruction; + + assert( + programId.equals(STAKE_POOL_PROGRAM_ID), + 'Invalid WithdrawStake instruction, program ID must be the Stake Pool Program' + ); + + const layoutCode = data.readUint8(0); + assert(layoutCode === WITHDRAW_STAKE_LAYOUT_CODE, 'Incorrect layout code in WithdrawStake data'); + assert(data.length === 9, 'Incorrect data size for WithdrawStake layout'); + const poolTokens = data.readBigInt64LE(1); + + let i = 0; + const stakePool = parseKey(keys[i++], { isSigner: false, isWritable: true }); + const validatorList = parseKey(keys[i++], { isSigner: false, isWritable: true }); + const withdrawAuthority = parseKey(keys[i++], { isSigner: false, isWritable: false }); + const validatorStake = parseKey(keys[i++], { isSigner: false, isWritable: true }); + const destinationStake = keys[i++].pubkey; // parseKey(keys[i++], { isSigner: false, isWritable: true }); + const destinationStakeAuthority = keys[i++].pubkey; // parseKey(keys[i++], { isSigner: false, isWritable: false }); + const sourceTransferAuthority = keys[i++].pubkey; // parseKey(keys[i++], { isSigner: true, isWritable: false }); + const sourcePoolAccount = parseKey(keys[i++], { isSigner: false, isWritable: true }); + const managerFeeAccount = parseKey(keys[i++], { isSigner: false, isWritable: true }); + const poolMint = parseKey(keys[i++], { isSigner: false, isWritable: true }); + const sysvarClockPubkey = parseKey(keys[i++], { isSigner: false, isWritable: false }); + assert(sysvarClockPubkey.equals(SYSVAR_CLOCK_PUBKEY), 'Unexpected pubkey in WithdrawStake instruction'); + const tokenProgramId = parseKey(keys[i++], { isSigner: false, isWritable: false }); + assert(tokenProgramId.equals(TOKEN_PROGRAM_ID), 'Unexpected pubkey in WithdrawStake instruction'); + const stakeProgramProgramId = parseKey(keys[i++], { isSigner: false, isWritable: false }); + assert(stakeProgramProgramId.equals(StakeProgram.programId), 'Unexpected pubkey in WithdrawStake instruction'); + assert(i === keys.length, 'Too many keys in WithdrawStake instruction'); + + return { + stakePool, + validatorList, + withdrawAuthority, + validatorStake, + destinationStake, + destinationStakeAuthority, + sourceTransferAuthority, + sourcePoolAccount, + managerFeeAccount, + poolMint, + poolTokens: Number(poolTokens), + }; +} diff --git a/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts b/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts index d93136caad..f9ecb0e5d8 100644 --- a/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts +++ b/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts @@ -1,4 +1,4 @@ -import { BaseCoin, NetworkType, SolCoin } from '@bitgo/statics'; +import { SolCoin } from '@bitgo/statics'; import { createAssociatedTokenAccountInstruction, createCloseAccountInstruction, @@ -6,6 +6,7 @@ import { createBurnInstruction, createTransferCheckedInstruction, TOKEN_2022_PROGRAM_ID, + createApproveInstruction, } from '@solana/spl-token'; import { Authorized, @@ -20,15 +21,7 @@ import { } from '@solana/web3.js'; import assert from 'assert'; import BigNumber from 'bignumber.js'; -import { - InstructionBuilderTypes, - JITO_MANAGER_FEE_ACCOUNT, - JITO_MANAGER_FEE_ACCOUNT_TESTNET, - JITO_STAKE_POOL_RESERVE_ACCOUNT, - JITO_STAKE_POOL_RESERVE_ACCOUNT_TESTNET, - JITOSOL_MINT_ADDRESS, - MEMO_PROGRAM_PK, -} from './constants'; +import { InstructionBuilderTypes, MEMO_PROGRAM_PK } from './constants'; import { AtaClose, AtaInit, @@ -47,9 +40,10 @@ import { WalletInit, SetPriorityFee, CustomInstruction, + Approve, } from './iface'; import { getSolTokenFromTokenName, isValidBase64, isValidHex } from './utils'; -import { depositSolInstructions } from './jitoStakePoolOperations'; +import { depositSolInstructions, withdrawStakeInstructions } from './jitoStakePoolOperations'; /** * Construct Solana instructions from instructions params @@ -57,10 +51,7 @@ import { depositSolInstructions } from './jitoStakePoolOperations'; * @param {InstructionParams} instructionToBuild - the data containing the instruction params * @returns {TransactionInstruction[]} An array containing supported Solana instructions */ -export function solInstructionFactory( - instructionToBuild: InstructionParams, - coinConfig: Readonly -): TransactionInstruction[] { +export function solInstructionFactory(instructionToBuild: InstructionParams): TransactionInstruction[] { switch (instructionToBuild.type) { case InstructionBuilderTypes.NonceAdvance: return advanceNonceInstruction(instructionToBuild); @@ -70,10 +61,12 @@ export function solInstructionFactory( return transferInstruction(instructionToBuild); case InstructionBuilderTypes.TokenTransfer: return tokenTransferInstruction(instructionToBuild); + case InstructionBuilderTypes.Approve: + return approveInstruction(instructionToBuild); case InstructionBuilderTypes.CreateNonceAccount: return createNonceAccountInstruction(instructionToBuild); case InstructionBuilderTypes.StakingActivate: - return stakingInitializeInstruction(instructionToBuild, coinConfig); + return stakingInitializeInstruction(instructionToBuild); case InstructionBuilderTypes.StakingDeactivate: return stakingDeactivateInstruction(instructionToBuild); case InstructionBuilderTypes.StakingWithdraw: @@ -223,6 +216,33 @@ function tokenTransferInstruction(data: TokenTransfer): TransactionInstruction[] return [transferInstruction]; } +/** + * Construct Transfer Solana instructions + * + * @param {Transfer} data - the data to build the instruction + * @returns {TransactionInstruction[]} An array containing Transfer Solana instruction + */ +function approveInstruction(data: Approve): TransactionInstruction[] { + const { + params: { accountAddress, delegateAddress, ownerAddress, amount, programId }, + } = data; + assert(accountAddress, 'Missing fromAddress (owner) param'); + assert(delegateAddress, 'Missing toAddress param'); + assert(ownerAddress, 'Missing ownerAddress param'); + assert(programId, 'Missing programId param'); + assert(amount, 'Missing amount param'); + return [ + createApproveInstruction( + new PublicKey(accountAddress), + new PublicKey(delegateAddress), + new PublicKey(ownerAddress), + BigInt(amount), + undefined, + programId === undefined ? undefined : new PublicKey(programId) + ), + ]; +} + /** * Construct Create and Initialize Nonce Solana instructions * @@ -252,9 +272,9 @@ function createNonceAccountInstruction(data: WalletInit): TransactionInstruction * @param {StakingActivate} data - the data to build the instruction * @returns {TransactionInstruction[]} An array containing Create Staking Account and Delegate Solana instructions */ -function stakingInitializeInstruction(data: StakingActivate, coinConfig: Readonly): TransactionInstruction[] { +function stakingInitializeInstruction(data: StakingActivate): TransactionInstruction[] { const { - params: { fromAddress, stakingAddress, amount, validator, isMarinade, isJito }, + params: { fromAddress, stakingAddress, amount, validator, isMarinade, isJito, jitoParams }, } = data; assert(fromAddress, 'Missing fromAddress param'); assert(stakingAddress, 'Missing stakingAddress param'); @@ -270,23 +290,15 @@ function stakingInitializeInstruction(data: StakingActivate, coinConfig: Readonl const tx = new Transaction(); if (isJito) { - const isTestnet = coinConfig.network.type === NetworkType.TESTNET; - const stakePoolMint = new PublicKey(JITOSOL_MINT_ADDRESS); - const stakePoolReserveStake = new PublicKey( - isTestnet ? JITO_STAKE_POOL_RESERVE_ACCOUNT_TESTNET : JITO_STAKE_POOL_RESERVE_ACCOUNT - ); - const stakePoolManagerFeeAccount = new PublicKey( - isTestnet ? JITO_MANAGER_FEE_ACCOUNT_TESTNET : JITO_MANAGER_FEE_ACCOUNT - ); + assert(jitoParams, 'missing jitoParams param'); + const instructions = depositSolInstructions( { stakePoolAddress: stakePubkey, from: fromPubkey, lamports: BigInt(amount), }, - stakePoolMint, - stakePoolReserveStake, - stakePoolManagerFeeAccount + jitoParams.stakePoolData ); tx.add(...instructions); } else if (isMarinade) { @@ -327,16 +339,38 @@ function stakingInitializeInstruction(data: StakingActivate, coinConfig: Readonl */ function stakingDeactivateInstruction(data: StakingDeactivate): TransactionInstruction[] { const { - params: { fromAddress, stakingAddress, isMarinade, recipients }, + params: { fromAddress, stakingAddress, amount, unstakingAddress, isMarinade, isJito, recipients, jitoParams }, } = data; assert(fromAddress, 'Missing fromAddress param'); if (!isMarinade) { assert(stakingAddress, 'Missing stakingAddress param'); } + assert([isMarinade, isJito].filter((x) => x).length <= 1, 'At most one of isMarinade and isJito can be true'); + + if (isJito) { + assert(unstakingAddress, 'Missing unstakingAddress param'); + assert(amount, 'Missing amount param'); + assert(jitoParams, 'Missing jitoParams param'); - if (isMarinade) { const tx = new Transaction(); + tx.add( + ...withdrawStakeInstructions( + { + stakePoolAddress: new PublicKey(stakingAddress), + tokenOwner: new PublicKey(fromAddress), + destinationStakeAccount: new PublicKey(unstakingAddress), + validatorAddress: new PublicKey(jitoParams.validatorAddress), + transferAuthority: new PublicKey(jitoParams.transferAuthorityAddress), + poolAmount: amount, + }, + jitoParams.stakePoolData + ) + ); + return tx.instructions; + } else if (isMarinade) { assert(recipients, 'Missing recipients param'); + + const tx = new Transaction(); const toPubkeyAddress = new PublicKey(recipients[0].address || ''); const transferInstruction = SystemProgram.transfer({ fromPubkey: new PublicKey(fromAddress), @@ -346,9 +380,7 @@ function stakingDeactivateInstruction(data: StakingDeactivate): TransactionInstr tx.add(transferInstruction); return tx.instructions; - } - - if (data.params.amount && data.params.unstakingAddress) { + } else if (data.params.amount && data.params.unstakingAddress) { const tx = new Transaction(); const unstakingAddress = new PublicKey(data.params.unstakingAddress); diff --git a/modules/sdk-coin-sol/src/lib/stakingActivateBuilder.ts b/modules/sdk-coin-sol/src/lib/stakingActivateBuilder.ts index 4ae3b81162..3bc7c2b48f 100644 --- a/modules/sdk-coin-sol/src/lib/stakingActivateBuilder.ts +++ b/modules/sdk-coin-sol/src/lib/stakingActivateBuilder.ts @@ -14,6 +14,7 @@ export class StakingActivateBuilder extends TransactionBuilder { protected _validator: string; protected _isMarinade = false; protected _isJito = false; + protected _jitoParams?: StakingActivate['params']['jitoParams']; constructor(_coinConfig: Readonly) { super(_coinConfig); @@ -35,6 +36,7 @@ export class StakingActivateBuilder extends TransactionBuilder { this.validator(activateInstruction.params.validator); this.isMarinade(activateInstruction.params.isMarinade ?? false); this.isJito(activateInstruction.params.isJito ?? false); + this.jitoParams(activateInstruction.params.jitoParams); } } } @@ -102,6 +104,17 @@ export class StakingActivateBuilder extends TransactionBuilder { return this; } + /** + * Set parameters specific to Jito staking. + * + * @param {string} jitoParams parameters specific to Jito staking. + * @returns {StakingActivateBuilder} This staking builder. + */ + jitoParams(jitoParams: StakingActivate['params']['jitoParams']): this { + this._jitoParams = jitoParams; + return this; + } + /** @inheritdoc */ protected async buildImplementation(): Promise { assert(this._sender, 'Sender must be set before building the transaction'); @@ -128,6 +141,7 @@ export class StakingActivateBuilder extends TransactionBuilder { validator: this._validator, isMarinade: this._isMarinade, isJito: this._isJito, + jitoParams: this._jitoParams, }, }; this._instructionsData = [stakingAccountData]; diff --git a/modules/sdk-coin-sol/src/lib/stakingDeactivateBuilder.ts b/modules/sdk-coin-sol/src/lib/stakingDeactivateBuilder.ts index adeb6d0662..694df516e7 100644 --- a/modules/sdk-coin-sol/src/lib/stakingDeactivateBuilder.ts +++ b/modules/sdk-coin-sol/src/lib/stakingDeactivateBuilder.ts @@ -14,7 +14,9 @@ export class StakingDeactivateBuilder extends TransactionBuilder { protected _amount?: string; protected _unstakingAddress: string; protected _isMarinade = false; + protected _isJito = false; protected _recipients: Recipient[]; + protected _jitoParams?: StakingDeactivate['params']['jitoParams']; constructor(_coinConfig: Readonly) { super(_coinConfig); @@ -31,18 +33,33 @@ export class StakingDeactivateBuilder extends TransactionBuilder { for (const instruction of this._instructionsData) { if (instruction.type === InstructionBuilderTypes.StakingDeactivate) { const deactivateInstruction: StakingDeactivate = instruction; - this.isMarinade(deactivateInstruction.params.isMarinade ?? false); + // Since _stakingAddresses needs to be populated, it gets special treatment. + stakingAddresses.push(deactivateInstruction.params.stakingAddress); + + // Marinade staking also cares about sender. if (!deactivateInstruction.params.isMarinade) { this.sender(deactivateInstruction.params.fromAddress); } - if (deactivateInstruction.params.isMarinade) { - this.recipients(deactivateInstruction.params.recipients ?? []); - } - stakingAddresses.push(deactivateInstruction.params.stakingAddress); - if (deactivateInstruction.params.amount && deactivateInstruction.params.unstakingAddress) { + + // The other values can just be copied. + if (deactivateInstruction.params.amount !== undefined) { this.amount(deactivateInstruction.params.amount); + } + if (deactivateInstruction.params.unstakingAddress !== undefined) { this.unstakingAddress(deactivateInstruction.params.unstakingAddress); } + if (deactivateInstruction.params.isMarinade !== undefined) { + this.isMarinade(deactivateInstruction.params.isMarinade); + } + if (deactivateInstruction.params.isJito !== undefined) { + this.isJito(deactivateInstruction.params.isJito); + } + if (deactivateInstruction.params.recipients !== undefined) { + this.recipients(deactivateInstruction.params.recipients); + } + if (deactivateInstruction.params.jitoParams !== undefined) { + this.jitoParams(deactivateInstruction.params.jitoParams); + } } } if (stakingAddresses.length > 1) { @@ -130,13 +147,34 @@ export class StakingDeactivateBuilder extends TransactionBuilder { /** * Set isMarinade flag * @param {boolean} flag - true if the transaction is for Marinade, false by default if not set - * @returns {StakingActivateBuilder} This staking builder + * @returns {StakingDectivateBuilder} This staking builder */ isMarinade(flag: boolean): this { this._isMarinade = flag; return this; } + /** + * Set isJito flag + * @param {boolean} flag - true if the transaction is for Jito, false by default if not set + * @returns {StakingDeactivateBuilder} This staking builder + */ + isJito(flag: boolean): this { + this._isJito = flag; + return this; + } + + /** + * Set parameters specific to Jito unstaking. + * + * @param {string} jitoParams parameters specific to Jito unstaking. + * @returns {StakingDeactivateBuilder} This staking builder. + */ + jitoParams(jitoParams: StakingDeactivate['params']['jitoParams']): this { + this._jitoParams = jitoParams; + return this; + } + /** @inheritdoc */ protected async buildImplementation(): Promise { assert(this._sender, 'Sender must be set before building the transaction'); @@ -155,7 +193,7 @@ export class StakingDeactivateBuilder extends TransactionBuilder { this._instructionsData.push(stakingDeactivateData); } } else { - if (!this._isMarinade) { + if (!this._isMarinade && !this._isJito) { // we don't need stakingAddress in marinade staking deactivate txn assert(this._stakingAddress, 'Staking address must be set before building the transaction'); } @@ -164,14 +202,15 @@ export class StakingDeactivateBuilder extends TransactionBuilder { throw new BuildTransactionError('Sender address cannot be the same as the Staking address'); } - if (this._amount && !this._isMarinade) { + if (this._amount && !this._isMarinade && !this._isJito) { assert( this._unstakingAddress, 'When partially unstaking the unstaking address must be set before building the transaction' ); } + this._instructionsData = []; - if (this._unstakingAddress && !this._isMarinade) { + if (this._unstakingAddress && !this._isMarinade && !this._isJito) { assert( this._amount, 'If an unstaking address is given then a partial amount to unstake must also be set before building the transaction' @@ -195,11 +234,14 @@ export class StakingDeactivateBuilder extends TransactionBuilder { amount: this._amount, unstakingAddress: this._unstakingAddress, isMarinade: this._isMarinade, + isJito: this._isJito, recipients: this._recipients, + jitoParams: this._jitoParams, }, }; this._instructionsData.push(stakingDeactivateData); } + return await super.buildImplementation(); } } diff --git a/modules/sdk-coin-sol/src/lib/transactionBuilder.ts b/modules/sdk-coin-sol/src/lib/transactionBuilder.ts index 8439ccee6e..79c95bff25 100644 --- a/modules/sdk-coin-sol/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-sol/src/lib/transactionBuilder.ts @@ -142,13 +142,13 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { if (this._nonceInfo) { tx.nonceInfo = { nonce: this._recentBlockhash, - nonceInstruction: solInstructionFactory(this._nonceInfo, this._coinConfig)[0], + nonceInstruction: solInstructionFactory(this._nonceInfo)[0], }; } else { tx.recentBlockhash = this._recentBlockhash; } for (const instruction of this._instructionsData) { - tx.add(...solInstructionFactory(instruction, this._coinConfig)); + tx.add(...solInstructionFactory(instruction)); } if (this._memo) { @@ -159,7 +159,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { }, }; this._instructionsData.push(memoData); - tx.add(...solInstructionFactory(memoData, this._coinConfig)); + tx.add(...solInstructionFactory(memoData)); } this._transaction.lamportsPerSignature = this._lamportsPerSignature; diff --git a/modules/sdk-coin-sol/src/lib/utils.ts b/modules/sdk-coin-sol/src/lib/utils.ts index f8ff013d5a..5857130574 100644 --- a/modules/sdk-coin-sol/src/lib/utils.ts +++ b/modules/sdk-coin-sol/src/lib/utils.ts @@ -9,15 +9,11 @@ import { import { BaseCoin, BaseNetwork, CoinNotDefinedError, coins, SolCoin } from '@bitgo/statics'; import { ASSOCIATED_TOKEN_PROGRAM_ID, - decodeCloseAccountInstruction, - decodeBurnInstruction, - decodeMintToInstruction, getAssociatedTokenAddress, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, - DecodedBurnInstruction, - DecodedMintToInstruction, - DecodedCloseAccountInstruction, + decodeInstruction, + TokenInstruction, } from '@solana/spl-token'; import { Keypair, @@ -54,6 +50,7 @@ import { ValidInstructionTypesEnum, walletInitInstructionIndexes, jitoStakingActivateInstructionsIndexes, + jitoStakingDeactivateInstructionsIndexes, } from './constants'; import { ValidInstructionTypes } from './iface'; import { STAKE_POOL_INSTRUCTION_LAYOUTS, STAKE_POOL_PROGRAM_ID } from '@solana/spl-stake-pool'; @@ -336,7 +333,8 @@ export function getTransactionType(transaction: SolTransaction): TransactionType } else if ( matchTransactionTypeByInstructionsOrder(instructions, marinadeStakingDeactivateInstructionsIndexes) || matchTransactionTypeByInstructionsOrder(instructions, stakingDeactivateInstructionsIndexes) || - matchTransactionTypeByInstructionsOrder(instructions, stakingPartialDeactivateInstructionsIndexes) + matchTransactionTypeByInstructionsOrder(instructions, stakingPartialDeactivateInstructionsIndexes) || + matchTransactionTypeByInstructionsOrder(instructions, jitoStakingDeactivateInstructionsIndexes) ) { return TransactionType.StakingDeactivate; } else if (matchTransactionTypeByInstructionsOrder(instructions, stakingWithdrawInstructionsIndexes)) { @@ -365,52 +363,18 @@ export function getInstructionType(instruction: TransactionInstruction): ValidIn return SystemInstruction.decodeInstructionType(instruction); case TOKEN_PROGRAM_ID.toString(): case TOKEN_2022_PROGRAM_ID.toString(): - try { - let decodedInstruction: DecodedCloseAccountInstruction | undefined; - if (instruction.programId.toString() !== TOKEN_2022_PROGRAM_ID.toString()) { - decodedInstruction = decodeCloseAccountInstruction(instruction); - } else { - decodedInstruction = decodeCloseAccountInstruction(instruction, TOKEN_2022_PROGRAM_ID); - } - if (decodedInstruction && decodedInstruction.data.instruction === 9) { - return 'CloseAssociatedTokenAccount'; - } - } catch (e) { - // ignore error and continue to check for other instruction types + const decodedInstruction = decodeInstruction(instruction, instruction.programId); + const instructionTypeMap: Map = new Map(); + instructionTypeMap.set(TokenInstruction.CloseAccount, 'CloseAssociatedTokenAccount'); + instructionTypeMap.set(TokenInstruction.Burn, 'Burn'); + instructionTypeMap.set(TokenInstruction.MintTo, 'MintTo'); + instructionTypeMap.set(TokenInstruction.Approve, 'Approve'); + instructionTypeMap.set(TokenInstruction.TransferChecked, 'TokenTransfer'); + const validInstruction = instructionTypeMap.get(decodedInstruction.data.instruction); + if (validInstruction === undefined) { + throw new Error(`Unsupported token instruction type ${decodedInstruction.data.instruction}`); } - - // Check for burn instructions (instruction code 8) - try { - let burnInstruction: DecodedBurnInstruction; - if (instruction.programId.toString() !== TOKEN_2022_PROGRAM_ID.toString()) { - burnInstruction = decodeBurnInstruction(instruction); - } else { - burnInstruction = decodeBurnInstruction(instruction, TOKEN_2022_PROGRAM_ID); - } - if (burnInstruction && burnInstruction.data.instruction === 8) { - return 'Burn'; - } - } catch (e) { - // ignore error and continue to check for other instruction types - } - - // Check for mint instructions (instruction code 7) - try { - let mintInstruction: DecodedMintToInstruction; - if (instruction.programId.toString() !== TOKEN_2022_PROGRAM_ID.toString()) { - mintInstruction = decodeMintToInstruction(instruction); - } else { - mintInstruction = decodeMintToInstruction(instruction, TOKEN_2022_PROGRAM_ID); - } - if (mintInstruction && mintInstruction.data.instruction === 7) { - return 'MintTo'; - } - } catch (e) { - // ignore error and continue to check for other instruction types - } - - // Default to TokenTransfer for other token instructions - return 'TokenTransfer'; + return validInstruction; case STAKE_POOL_PROGRAM_ID.toString(): const discriminator = instruction.data.readUint8(0); const layoutKey = Object.entries(STAKE_POOL_INSTRUCTION_LAYOUTS).find(([_, v]) => v.index === discriminator)?.[0]; diff --git a/modules/sdk-coin-sol/test/resources/sol.ts b/modules/sdk-coin-sol/test/resources/sol.ts index 3de105a434..6ed7362ef4 100644 --- a/modules/sdk-coin-sol/test/resources/sol.ts +++ b/modules/sdk-coin-sol/test/resources/sol.ts @@ -1,3 +1,5 @@ +import { StakePool, StakePoolLayout } from '@solana/spl-stake-pool'; + export const prvKeys = { prvKey1: { base58: '5jtsd9SmUH5mFZL7ywNNmqmxxdXw4FQ5GJQprXmrdX4LCuUwMBivCfUX2ar8hGdnLHDGrVKkshW1Ke21vZhPiLyr', @@ -158,7 +160,7 @@ export const MARINADE_STAKING_ACTIVATE_SIGNED_TX = 'AuRFS0r7hJ+/+WuDQbbwdjSgxfnKOWi94EnWEha9uaBPt8VZOXiOoSiSoES34VkyBNLlLqlfK0fP3d5eJR+srQvN04gqzpOZPTVzqiomyMXqwQ6FYoQg5nEkdiDVny8SsyhRnAeDMzexkKD+3rwSGP0E+XN/2crTL6PZRnip42YFAgADBUXlebz5JTz2i0ff8fs6OlwsIbrFsjwJrhKm4FVr8ItBYnsvugEnYfm5Gbz5TLtMncgFHZ8JMpkxTTlJIzJovekAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAah2BeRN1QqmDQ3vf4qerJVf1NcinhyK2ikncAAAAAABqfVFxksXFEhjMlMPUrxf1ja7gibof1E49vZigAAAADjMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQICAgABNAAAAADgkwQAAAAAAMgAAAAAAAAABqHYF5E3VCqYNDe9/ip6slV/U1yKeHIraKSdwAAAAAADAgEEdAAAAACx+Xl4mhxH0TxI2HovJxcQ63+TJglRFzFikL1sKdr12UXlebz5JTz2i0ff8fs6OlwsIbrFsjwJrhKm4FVr8ItBAAAAAAAAAAAAAAAAAAAAAEXlebz5JTz2i0ff8fs6OlwsIbrFsjwJrhKm4FVr8ItB'; export const JITO_STAKING_ACTIVATE_SIGNED_TX = - 'AXagGlsxiPLvRLV3WaC9YMjBrUlPISHX0xIMRtrHvYLJyRVfyPpCabQcE8aBvXkz95moq1Q+jROMQzXZOWkTXQ0BAAYMReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0Ecg6pe+BOG2OETfAVS9ftz6va1oE4onLBolJ2N+ZOOhLZtVF4GKFOAVZnUC4e8/Xev/fZDinazOce3aExJcA5A/NFB6YMsrxCtkXSVyg8nG1spPNRwJ+pzcAftQOs5oL0Eij4Iw7SVvhf0VCfYm+xbgMfiaVwYZNdnQ9s5vtNG1gzFbtdH7X73TLJSD5AC95Nt/ZLk3yZg4fDJYZ3WbbyJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABU5Z4kwFGooUp7HpeX8OEs36dJAhZlMZWmpRKm8WZgK4yXJY9OJInxuz0QKRSODYMLWhOZ2v8QhASOe9jb6fhZBoFO1Mr2ihdGcv2shgMaY+hOoV76HUS3IpP229sAFlAGp9UXGSxcUSGMyUw9SvF/WNruCJuh/UTj29mKAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp4zLa+S+r7Oi2P/ekQAXl/f2a+hWHVrYcWpX5BLO40IECCAcAAQADBgsKAAkKBAcFAAECAQMGCwkO4JMEAAAAAAA='; + 'AYGveJCjqzABgyHV02jCHAff28D2a2Sdsd8FP3c0ns6edXoH/2rIfRfCbyNvVSv92r0kpJnKhci9A8TALeb5iQUBAAYMReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0Ecg6pe+BOG2OETfAVS9ftz6va1oE4onLBolJ2N+ZOOhJ6naP7fZEyKrpuOIYit0GvFUPv3Fsgiuc5jx3g9lS4fCeaj/uz5kDLhwd9rlyLcs2NOe440QJNrw0sMwcjrUh/80UHpgyyvEK2RdJXKDycbWyk81HAn6nNwB+1A6zmgvQSKPgjDtJW+F/RUJ9ib7FuAx+JpXBhk12dD2zm+00bWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABU5Z4kwFGooUp7HpeX8OEs36dJAhZlMZWmpRKm8WZgK4yXJY9OJInxuz0QKRSODYMLWhOZ2v8QhASOe9jb6fhZBoFO1Mr2ihdGcv2shgMaY+hOoV76HUS3IpP229sAFlAGp9UXGSxcUSGMyUw9SvF/WNruCJuh/UTj29mKAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp4zLa+S+r7Oi2P/ekQAXl/f2a+hWHVrYcWpX5BLO40IECCAcAAQAEBgsKAAkKBQcCAAEDAQQGCwkO4JMEAAAAAAA='; export const STAKING_ACTIVATE_SIGNED_TX_WITH_MEMO = 'AsTWc6tgb0h6qBA/kcVgr35lpWYxit9d99IscSJ5OUHkTz4AUK0dI7MNX9kw1GMIvxGKg7uw709b/9K1CeUgRgHdrX1nKO30P/91RhNMJpknfdDHmq48duVvvPRhlXirbMNm0yqn2q4iEWk3U8pS4ASPAU2L0jlk1NSqnw5sxMcOAgAICkXlebz5JTz2i0ff8fs6OlwsIbrFsjwJrhKm4FVr8ItBYnsvugEnYfm5Gbz5TLtMncgFHZ8JMpkxTTlJIzJovekAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALH5eXiaHEfRPEjYei8nFxDrf5MmCVEXMWKQvWwp2vXZBUpTWpkpIQZNJOhxYNo4fHw1td28kruB5B+oQEEFRI0GodgXkTdUKpg0N73+KnqyVX9TXIp4citopJ3AAAAAAAah2BelAgULaAeR5s5tuI4eW3FQ9h/GeQpOtNEAAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAAAGp9UXGSxcUSGMyUw9SvF/WNruCJuh/UTj29mKAAAAAAan1RcZNYTQ/u2bs0MdEyBr5UQoG1e4VmzFN1/0AAAA4zLa+S+r7Oi2P/ekQAXl/f2a+hWHVrYcWpX5BLO40IEEAgIAATQAAAAA4JMEAAAAAADIAAAAAAAAAAah2BeRN1QqmDQ3vf4qerJVf1NcinhyK2ikncAAAAAABQIBCHQAAAAAReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0FF5Xm8+SU89otH3/H7OjpcLCG6xbI8Ca4SpuBVa/CLQQAAAAAAAAAAAAAAAAAAAABF5Xm8+SU89otH3/H7OjpcLCG6xbI8Ca4SpuBVa/CLQQUGAQMHCQYABAIAAAAEAAl0ZXN0IG1lbW8='; @@ -184,6 +186,9 @@ export const STAKING_DEACTIVATE_SIGNED_TX = export const MARINADE_STAKING_DEACTIVATE_SIGNED_TX = 'AaiHUOzzeJaUzpdkm2BmLJI3AVOhFHTD5BnVMwUH3lRv8hKpH1fnXiXNm6ghZgNwhggXBAqhL3t4XEl+H7T95gIBAAIEReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0EL/kczKamI94jNtLT/BR9nkfa/PR2IU6d7qaEV8VVC3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUpTWpkpIQZNJOhxYNo4fHw1td28kruB5B+oQEEFRI3jMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQICAgABDAIAAABgGCMAAAAAAAMAbntcIlByZXBhcmVGb3JSZXZva2VcIjp7XCJ1c2VyXCI6XCI1aHI1ZmlzUGk2RFhOdXVScG01WFVienBpRW5tZHl4WHVCRFR3endaajVQZX1cIixcImFtb3VudFwiOlwiNTAwMDAwMDAwMDAwXCJ9'; +export const JITO_STAKING_DEACTIVATE_SIGNED_TX = + 'AxwT9X//RNqhBUEE7kjLJcjd03q4oMb3KaNjjdUzCAujT1fhurTRZpezP9GmRuDONrV2fdBb+yzBEqXDg4HsFgrU0VQsePg3jaWTrome8N7kTrjs5UMX3LlRIGDlWugEDsPDUCBVF6emwL1gpjM4Zwxu/tltXum2pP0i51EtRIEHX+QbTMqkXY62U5lzJBeuMQ4Y9NVU0oEyffdOktzeFxeIooU5QUu13TfeoKWbjCvumfMTIB+bTaWAxLp9fKvDCwMBBg9F5Xm8+SU89otH3/H7OjpcLCG6xbI8Ca4SpuBVa/CLQWJ7L7oBJ2H5uRm8+Uy7TJ3IBR2fCTKZMU05SSMyaL3p6Lfi7T4mzoclKbPedsv+JDs60KtRcBK6Y7CHyYejKikcg6pe+BOG2OETfAVS9ftz6va1oE4onLBolJ2N+ZOOhCPgdQm63e39tRapC5GXu1BHQyVdDjfF/13OiiQe7cQxl9rD5vZLBx1Aaz7SV4hmv2ZhGp4LQEU67b++EtDrIi8J5qP+7PmQMuHB32uXItyzY057jjRAk2vDSwzByOtSH/zRQemDLK8QrZF0lcoPJxtbKTzUcCfqc3AH7UDrOaC9BIo+CMO0lb4X9FQn2JvsW4DH4mlcGGTXZ0PbOb7TRtYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFTlniTAUaihSnsel5fw4Szfp0kCFmUxlaalEqbxZmArBoFO1Mr2ihdGcv2shgMaY+hOoV76HUS3IpP229sAFlAGodgXkTdUKpg0N73+KnqyVX9TXIp4citopJ3AAAAAAAan1RcYx3TJKFZjmGkdXraLXrijm0ttXHNVWyEAAAAABt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKnjMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQMOAwMAAAkE6AMAAAAAAAAJAgABNAAAAACA1SIAAAAAAMgAAAAAAAAABqHYF5E3VCqYNDe9/ip6slV/U1yKeHIraKSdwAAAAAALDQgECgUBAAIDBgcNDgwJCugDAAAAAAAA'; + export const STAKING_DEACTIVATE_SIGNED_TX_single = 'AUfyWtl4IUxhH21qX/H03hJZer1XxQaxL2r/uDTM/u1GzBIyePCHu78O2SkWGEYP6eDdiY3OLfJmUM1jiy8NCAoBAAIEReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0Fiey+6ASdh+bkZvPlMu0ydyAUdnwkymTFNOUkjMmi96Qah2BeRN1QqmDQ3vf4qerJVf1NcinhyK2ikncAAAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAADjMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQECAwEDAAQFAAAA'; @@ -463,3 +468,12 @@ export const MULTI_NATIVE_AND_TOKEN_TRANSFERV2_UNSIGNED = 'AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAECNPhdfVnHvE4DTw6gLpHWddJ/hL8IhXVdS3cKq2ekMAfReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0EAxPE0fqi8zYGKbihafYdJgknwg8wIfK86jxD3ILOvGakYG1L37ZDq6w2tS3G+tFODYWdhMXF+kwlYEF+3o4nVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAANEDifvO5SjyCGEdzMc0sxCSVAyyuNWNEA8uqizttNpeBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKnjMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQUFAAkDgJaYAAAAAAAEAgEDDAIAAADgkwQAAAAAAAcEAgYDAQoM4JMEAAAAAAAJBwQCBgMBCgzgkwQAAAAAAAkHBAIGAwEKDOCTBAAAAAAACQ=='; export const TOKEN_TRANSFERV2_SIGNED_TX_WITH_WITH_CREATE_ATA_AND_MEMO_AND_DURABLE_NONCE_WITH_OPTIONAL_PARAMS = 'AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAClsk0XmHek318x15eAkXEhQy6js9rm/P/UkDf7zyfLMjA8wmCpzLlxleu5hkrE4WQyFkHtR2qqTTLCa5ZHgp4BAgAIDtPhdfVnHvE4DTw6gLpHWddJ/hL8IhXVdS3cKq2ekMAfAGymKVqOJEQemBHH67uu8ISJV4rtwTejLrjw7VSeW6dstKPy47z/Dq4I02mhBXGTbP9R3C+quPu54TJWzf9ohW/6Eon6nFlovAcYTKTZK+nk98ALlkSL/BgjW09MceFuqRgbUvftkOrrDa1Lcb60U4NhZ2ExcX6TCVgQX7ejidXPunKtWdaUXgDtculuknl1oO5Dz7CHrwvjz6emEVw9uQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkDBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAANEDifv3IHbw1cr91SvksbLsVBROfMcuHJXdMSzttNpeBUpTWpkpIQZNJOhxYNo4fHw1td28kruB5B+oQEEFRI0Gp9UXGSxWjuCKhF9z0peIzwNcMUWyGrNE2AYuqUAAAAan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAABt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKnjMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQUGAwMLAQQEAAAACAAJA4CWmAAAAAAABwcBAgQJBg0MAA0EBQkEAQoM4JMEAAAAAAAJCgAJdGVzdCBtZW1v'; + +export const JITO_STAKE_POOL_DATA_ENCODED = + 'AUUePdUNO3uFNgRcK3rC7CWUc+vCWuO8vh++sX1S+8e+eXhXwruGsaac0PTcoWwisNzj3eyWuEBcCPHEcDrQj9NUtd6+o5sz4PHc+gqPYiqVuLTrluhPL6HjF2cPHpbB2P0j4HUJut3t/bUWqQuRl7tQR0MlXQ43xf9dzookHu3EMZ6naP7fZEyKrpuOIYit0GvFUPv3Fsgiuc5jx3g9lS4f/NFB6YMsrxCtkXSVyg8nG1spPNRwJ+pzcAftQOs5oL0J5qP+7PmQMuHB32uXItyzY057jjRAk2vDSwzByOtSHwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpMX/jsf9cOACrkYQCkRwuAD0DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADoAwAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6AMAAAAAAAABAAAAAAAAAADS9oX3AxMuAHGG8eUtTDgAAwAAAAAAAAEAAAAAAAAAAPdO5G5V4RQApItSanmLFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='; + +export const JITO_STAKE_POOL_DATA_PARSED: StakePool = StakePoolLayout.decode( + Buffer.from(JITO_STAKE_POOL_DATA_ENCODED, 'base64') +); + +export const JITO_STAKE_POOL_VALIDATOR_ADDRESS = 'BDn3HiXMTym7ZQofWFxDb7ZGQX6GomQzJYKfytTAqd5g'; diff --git a/modules/sdk-coin-sol/test/unit/solInstructionFactory.ts b/modules/sdk-coin-sol/test/unit/solInstructionFactory.ts index 1adb43ce29..19605eae7b 100644 --- a/modules/sdk-coin-sol/test/unit/solInstructionFactory.ts +++ b/modules/sdk-coin-sol/test/unit/solInstructionFactory.ts @@ -12,9 +12,6 @@ import { TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; import BigNumber from 'bignumber.js'; -import { coins } from '@bitgo/statics'; - -const COIN_CONFIG = coins.get('tsol'); describe('Instruction Builder Tests: ', function () { describe('Succeed ', function () { @@ -25,7 +22,7 @@ describe('Instruction Builder Tests: ', function () { params: { memo }, }; - const result = solInstructionFactory(memoParams, COIN_CONFIG); + const result = solInstructionFactory(memoParams); should.deepEqual(result, [ new TransactionInstruction({ keys: [], @@ -44,7 +41,7 @@ describe('Instruction Builder Tests: ', function () { params: { fromAddress, toAddress, amount }, }; - const result = solInstructionFactory(transferParams, COIN_CONFIG); + const result = solInstructionFactory(transferParams); should.deepEqual(result, [ SystemProgram.transfer({ fromPubkey: new PublicKey(fromAddress), @@ -62,7 +59,7 @@ describe('Instruction Builder Tests: ', function () { params: { walletNonceAddress, authWalletAddress }, }; - const result = solInstructionFactory(nonceAdvanceParams, COIN_CONFIG); + const result = solInstructionFactory(nonceAdvanceParams); should.deepEqual(result, [ SystemProgram.nonceAdvance({ noncePubkey: new PublicKey(walletNonceAddress), @@ -81,7 +78,7 @@ describe('Instruction Builder Tests: ', function () { params: { fromAddress, nonceAddress, authAddress, amount }, }; - const result = solInstructionFactory(createNonceAccountParams, COIN_CONFIG); + const result = solInstructionFactory(createNonceAccountParams); should.deepEqual( result, SystemProgram.createNonceAccount({ @@ -109,7 +106,7 @@ describe('Instruction Builder Tests: ', function () { }, }; - const result = solInstructionFactory(createATAParams, COIN_CONFIG); + const result = solInstructionFactory(createATAParams); should.deepEqual(result, [ createAssociatedTokenAccountInstruction( new PublicKey(payerAddress), @@ -139,7 +136,7 @@ describe('Instruction Builder Tests: ', function () { }, }; - const result = solInstructionFactory(transferParams, COIN_CONFIG); + const result = solInstructionFactory(transferParams); should.deepEqual(result, [ createTransferCheckedInstruction( new PublicKey(sourceAddress), @@ -172,7 +169,7 @@ describe('Instruction Builder Tests: ', function () { }, }; - const result = solInstructionFactory(mintParams, COIN_CONFIG); + const result = solInstructionFactory(mintParams); should.deepEqual(result, [ createMintToInstruction( new PublicKey(mintAddress), @@ -204,7 +201,7 @@ describe('Instruction Builder Tests: ', function () { }, }; - const result = solInstructionFactory(mintParams, COIN_CONFIG); + const result = solInstructionFactory(mintParams); should.deepEqual(result, [ createMintToInstruction( new PublicKey(mintAddress), @@ -237,7 +234,7 @@ describe('Instruction Builder Tests: ', function () { }, }; - const result = solInstructionFactory(burnParams, COIN_CONFIG); + const result = solInstructionFactory(burnParams); should.deepEqual(result, [ createBurnInstruction( new PublicKey(accountAddress), @@ -269,7 +266,7 @@ describe('Instruction Builder Tests: ', function () { }, }; - const result = solInstructionFactory(burnParams, COIN_CONFIG); + const result = solInstructionFactory(burnParams); should.deepEqual(result, [ createBurnInstruction( new PublicKey(accountAddress), @@ -300,7 +297,7 @@ describe('Instruction Builder Tests: ', function () { }, }; - const result = solInstructionFactory(mintParams, COIN_CONFIG); + const result = solInstructionFactory(mintParams); should.deepEqual(result, [ createMintToInstruction( new PublicKey(mintAddress), @@ -329,7 +326,7 @@ describe('Instruction Builder Tests: ', function () { }, }; - const result = solInstructionFactory(burnParams, COIN_CONFIG); + const result = solInstructionFactory(burnParams); should.deepEqual(result, [ createBurnInstruction( new PublicKey(accountAddress), @@ -360,7 +357,7 @@ describe('Instruction Builder Tests: ', function () { }, }; - const result = solInstructionFactory(customInstructionParams, COIN_CONFIG); + const result = solInstructionFactory(customInstructionParams); result.should.have.length(1); const resultInstruction = result[0]; @@ -393,7 +390,7 @@ describe('Instruction Builder Tests: ', function () { }, }; - const result = solInstructionFactory(customInstructionParams, COIN_CONFIG); + const result = solInstructionFactory(customInstructionParams); result.should.have.length(1); const resultInstruction = result[0]; @@ -421,7 +418,7 @@ describe('Instruction Builder Tests: ', function () { }, }; - const result = solInstructionFactory(customInstructionParams, COIN_CONFIG); + const result = solInstructionFactory(customInstructionParams); result.should.have.length(1); const resultInstruction = result[0]; @@ -436,7 +433,7 @@ describe('Instruction Builder Tests: ', function () { describe('Fail ', function () { it('Invalid type', () => { // @ts-expect-error Testing for an invalid type, should throw error - should(() => solInstructionFactory({ type: 'random', params: {} }, COIN_CONFIG)).throwError( + should(() => solInstructionFactory({ type: 'random', params: {} })).throwError( 'Invalid instruction type or not supported' ); }); @@ -450,7 +447,7 @@ describe('Instruction Builder Tests: ', function () { }, } as unknown as InstructionParams; - should(() => solInstructionFactory(customInstructionParams, COIN_CONFIG)).throwError( + should(() => solInstructionFactory(customInstructionParams)).throwError( 'Missing programId in custom instruction' ); }); @@ -464,7 +461,7 @@ describe('Instruction Builder Tests: ', function () { }, } as unknown as InstructionParams; - should(() => solInstructionFactory(customInstructionParams, COIN_CONFIG)).throwError( + should(() => solInstructionFactory(customInstructionParams)).throwError( 'Missing or invalid keys in custom instruction' ); }); @@ -484,9 +481,7 @@ describe('Instruction Builder Tests: ', function () { }, } as unknown as InstructionParams; - should(() => solInstructionFactory(customInstructionParams, COIN_CONFIG)).throwError( - 'Missing data in custom instruction' - ); + should(() => solInstructionFactory(customInstructionParams)).throwError('Missing data in custom instruction'); }); }); }); diff --git a/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingActivateBuilder.ts b/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingActivateBuilder.ts index daa8136aa4..86908342fc 100644 --- a/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingActivateBuilder.ts +++ b/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingActivateBuilder.ts @@ -105,6 +105,13 @@ describe('Sol Staking Activate Builder', () => { .sender(wallet.pub) .stakingAddress(JITO_STAKE_POOL_ADDRESS) .validator(JITO_STAKE_POOL_ADDRESS) + .jitoParams({ + stakePoolData: { + managerFeeAccount: testData.JITO_STAKE_POOL_DATA_PARSED.managerFeeAccount.toString(), + poolMint: testData.JITO_STAKE_POOL_DATA_PARSED.poolMint.toString(), + reserveStake: testData.JITO_STAKE_POOL_DATA_PARSED.reserveStake.toString(), + }, + }) .isJito(true) .nonce(recentBlockHash); txBuilder.sign({ key: wallet.prv }); @@ -130,6 +137,13 @@ describe('Sol Staking Activate Builder', () => { validator: JITO_STAKE_POOL_ADDRESS, isMarinade: false, isJito: true, + jitoParams: { + stakePoolData: { + managerFeeAccount: testData.JITO_STAKE_POOL_DATA_PARSED.managerFeeAccount.toString(), + poolMint: testData.JITO_STAKE_POOL_DATA_PARSED.poolMint.toString(), + reserveStake: testData.JITO_STAKE_POOL_DATA_PARSED.reserveStake.toString(), + }, + }, }, }, ]); diff --git a/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingDeactivateBuilder.ts b/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingDeactivateBuilder.ts index ff7e548df0..665489bdfa 100644 --- a/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingDeactivateBuilder.ts +++ b/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingDeactivateBuilder.ts @@ -5,6 +5,7 @@ import { KeyPair, Utils } from '../../../src'; import * as testData from '../../resources/sol'; import { Recipient, TransactionType } from '@bitgo/sdk-core'; import * as bs58 from 'bs58'; +import { JITO_STAKE_POOL_ADDRESS } from '../../../src/lib/constants'; describe('Sol Staking Deactivate Builder', () => { const factory = getBuilderFactory('tsol'); @@ -122,6 +123,63 @@ describe('Sol Staking Deactivate Builder', () => { should.equal(rawSignedTx, testData.MARINADE_STAKING_DEACTIVATE_SIGNED_TX); }); + it('Jito: build and sign a staking deactivate tx', async () => { + const txBuilder = factory.getStakingDeactivateBuilder(); + const transferAuthority = new KeyPair(testData.splitStakeAccount).getKeys(); + txBuilder + .sender(wallet.pub) + .isJito(true) + .stakingAddress(JITO_STAKE_POOL_ADDRESS) + .unstakingAddress(stakeAccount.pub) + .jitoParams({ + validatorAddress: testData.JITO_STAKE_POOL_VALIDATOR_ADDRESS, + transferAuthorityAddress: transferAuthority.pub, + stakePoolData: { + managerFeeAccount: testData.JITO_STAKE_POOL_DATA_PARSED.managerFeeAccount.toString(), + poolMint: testData.JITO_STAKE_POOL_DATA_PARSED.poolMint.toString(), + validatorList: testData.JITO_STAKE_POOL_DATA_PARSED.validatorList.toString(), + }, + }) + .amount('1000') + .nonce(recentBlockHash); + const txUnsigned = await txBuilder.build(); + txBuilder.sign({ key: wallet.prv }); + txBuilder.sign({ key: stakeAccount.prv }); + txBuilder.sign({ key: transferAuthority.prv }); + const tx = await txBuilder.build(); + const txJson = tx.toJson(); + const rawTx = tx.toBroadcastFormat(); + should.equal(Utils.isValidRawTransaction(rawTx), true); + txJson.instructionsData.should.deepEqual([ + { + type: 'Deactivate', + params: { + fromAddress: wallet.pub, + isMarinade: false, + isJito: true, + stakingAddress: JITO_STAKE_POOL_ADDRESS, + unstakingAddress: stakeAccount.pub, + amount: '1000', + recipients: undefined, + jitoParams: { + validatorAddress: testData.JITO_STAKE_POOL_VALIDATOR_ADDRESS, + transferAuthorityAddress: transferAuthority.pub, + stakePoolData: { + managerFeeAccount: testData.JITO_STAKE_POOL_DATA_PARSED.managerFeeAccount.toString(), + poolMint: testData.JITO_STAKE_POOL_DATA_PARSED.poolMint.toString(), + validatorList: testData.JITO_STAKE_POOL_DATA_PARSED.validatorList.toString(), + }, + }, + }, + }, + ]); + should.equal(rawTx, testData.JITO_STAKING_DEACTIVATE_SIGNED_TX); + + const tx2 = await factory.from(txUnsigned.toBroadcastFormat()).build(); + should.equal(tx2.toBroadcastFormat(), txUnsigned.toBroadcastFormat()); + should.equal(tx2.signablePayload.toString('hex'), txUnsigned.signablePayload.toString('hex')); + }); + it('building a staking multi deactivate tx', async () => { const txBuilder = factory.getStakingDeactivateBuilder(); txBuilder.sender(wallet.pub).stakingAddresses([stakeAccount.pub, splitAccount.pub]).nonce(recentBlockHash); From 5cc2722a19bae5c195d52eaccc973f9c64a08abf Mon Sep 17 00:00:00 2001 From: Harit Kapadia Date: Fri, 8 Aug 2025 09:52:36 -0400 Subject: [PATCH 2/2] refactor(sdk-coin-sol): change marinade and jito flags to enum BREAKING CHANGE: the `isMarinade` setter in transaction builders has been removed. Existing calls to `txBuilder.isMarinade` will have to be replaced with `txBuilder.stakingType(StakingType.MARINADE)`. Ticket: SC-2620 --- examples/ts/sol/stake-jito.ts | 4 +- modules/sdk-coin-sol/src/lib/iface.ts | 36 +- .../src/lib/instructionParamsFactory.ts | 350 ++++++++++++------ .../src/lib/jitoStakePoolOperations.ts | 12 +- .../src/lib/solInstructionFactory.ts | 263 +++++++------ .../src/lib/stakingActivateBuilder.ts | 53 +-- .../src/lib/stakingDeactivateBuilder.ts | 66 ++-- .../unit/instructionParamsFactory.staking.ts | 36 +- .../stakingActivateBuilder.ts | 30 +- .../stakingDeactivateBuilder.ts | 47 +-- .../transactionBuilder/transactionBuilder.ts | 4 +- 11 files changed, 503 insertions(+), 398 deletions(-) diff --git a/examples/ts/sol/stake-jito.ts b/examples/ts/sol/stake-jito.ts index c9aebc9e77..281ffe70da 100644 --- a/examples/ts/sol/stake-jito.ts +++ b/examples/ts/sol/stake-jito.ts @@ -59,8 +59,8 @@ async function main() { .sender(account.publicKey.toBase58()) .stakingAddress(JITO_STAKE_POOL_ADDRESS) .validator(JITO_STAKE_POOL_ADDRESS) - .isJito(true) - .jitoParams({ + .stakingTypeParams({ + type: 'JITO', stakePoolData: { managerFeeAccount: stakePoolAccount.account.data.managerFeeAccount.toString(), poolMint: stakePoolAccount.account.data.poolMint.toString(), diff --git a/modules/sdk-coin-sol/src/lib/iface.ts b/modules/sdk-coin-sol/src/lib/iface.ts index 60cf3dc0b7..36eaf81497 100644 --- a/modules/sdk-coin-sol/src/lib/iface.ts +++ b/modules/sdk-coin-sol/src/lib/iface.ts @@ -120,6 +120,18 @@ export interface Approve { }; } +export enum StakingType { + NATIVE = 'NATIVE', + MARINADE = 'MARINADE', + JITO = 'JITO', +} + +export interface JitoStakingActivateParams { + stakePoolData: DepositSolStakePoolData; +} + +export type StakingActivateExtraParams = JitoStakingActivateParams; + export interface StakingActivate { type: InstructionBuilderTypes.StakingActivate; params: { @@ -127,11 +139,8 @@ export interface StakingActivate { stakingAddress: string; amount: string; validator: string; - isMarinade?: boolean; - isJito?: boolean; - jitoParams?: { - stakePoolData: DepositSolStakePoolData; - }; + stakingType: StakingType; + extraParams?: StakingActivateExtraParams; }; } @@ -140,6 +149,14 @@ export interface StakingDelegate { params: { stakingAddress: string; fromAddress: string; validator: string }; } +export interface JitoStakingDeactivateParams { + stakePoolData: WithdrawStakeStakePoolData; + validatorAddress: string; + transferAuthorityAddress: string; +} + +export type StakingDeactivateExtraParams = JitoStakingDeactivateParams; + export interface StakingDeactivate { type: InstructionBuilderTypes.StakingDeactivate; params: { @@ -147,13 +164,8 @@ export interface StakingDeactivate { stakingAddress: string; amount?: string; unstakingAddress?: string; - isMarinade?: boolean; - isJito?: boolean; - jitoParams?: { - stakePoolData: WithdrawStakeStakePoolData; - validatorAddress: string; - transferAuthorityAddress: string; - }; + stakingType: StakingType; + extraParams?: StakingDeactivateExtraParams; recipients?: Recipient[]; }; } diff --git a/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts b/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts index 97b00b9a1c..59e3b99f1f 100644 --- a/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts +++ b/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts @@ -11,7 +11,6 @@ import { import { AllocateParams, AssignParams, - AuthorizeStakeParams, CreateAccountParams, DeactivateStakeParams, DecodedTransferInstruction, @@ -28,12 +27,7 @@ import { import { NotSupported, TransactionType } from '@bitgo/sdk-core'; import { coins, SolCoin } from '@bitgo/statics'; import assert from 'assert'; -import { - InstructionBuilderTypes, - JITO_STAKE_POOL_ADDRESS, - ValidInstructionTypesEnum, - walletInitInstructionIndexes, -} from './constants'; +import { InstructionBuilderTypes, ValidInstructionTypesEnum, walletInitInstructionIndexes } from './constants'; import { AtaClose, AtaInit, @@ -53,6 +47,7 @@ import { SetPriorityFee, CustomInstruction, Approve, + StakingType, } from './iface'; import { getInstructionType } from './utils'; import { DepositSolParams, WithdrawStakeParams } from '@solana/spl-stake-pool'; @@ -330,12 +325,49 @@ function parseSendInstructions( return instructionData; } -function stakingInstructionsIsMarinade(si: StakingInstructions): boolean { - return !!(si.delegate === undefined && si.depositSol === undefined); +type StakingInstructions = { + depositSol?: DepositSolParams; + create?: CreateAccountParams; + initialize?: InitializeStakeParams; + delegate?: DelegateStakeParams; +}; + +type JitoStakingInstructions = StakingInstructions & { + depositSol: NonNullable; +}; + +function isJitoStakingInstructions(si: StakingInstructions): si is JitoStakingInstructions { + return si.depositSol !== undefined; +} + +type MarinadeStakingInstructions = StakingInstructions & { + create: NonNullable; + initialize: NonNullable; +}; + +function isMarinadeStakingInstructions(si: StakingInstructions): si is MarinadeStakingInstructions { + return si.create !== undefined && si.initialize !== undefined && si.delegate === undefined; +} + +type NativeStakingInstructions = StakingInstructions & { + create: NonNullable; + initialize: NonNullable; + delegate: NonNullable; +}; + +function isNativeStakingInstructions(si: StakingInstructions): si is NativeStakingInstructions { + return si.create !== undefined && si.initialize !== undefined && si.delegate !== undefined; } -function stakingInstructionsIsJito(si: StakingInstructions): boolean { - return !!(si.delegate === undefined && si.depositSol?.stakePool.toString() === JITO_STAKE_POOL_ADDRESS); +function getStakingTypeFromStakingInstructions(si: StakingInstructions): StakingType { + const isJito = isJitoStakingInstructions(si); + const isMarinade = isMarinadeStakingInstructions(si); + const isNative = isNativeStakingInstructions(si); + assert([isJito, isMarinade, isNative].filter((x) => x).length === 1, 'StakingType is ambiguous'); + if (isJito) return StakingType.JITO; + if (isMarinade) return StakingType.MARINADE; + if (isNative) return StakingType.NATIVE; + assert(false, 'No StakingType found'); } /** @@ -402,40 +434,72 @@ function parseStakingActivateInstructions( } validateStakingInstructions(stakingInstructions); + const stakingType = getStakingTypeFromStakingInstructions(stakingInstructions); - const stakingActivate: StakingActivate = { - type: InstructionBuilderTypes.StakingActivate, - params: { - fromAddress: - stakingInstructions.create?.fromPubkey.toString() || - stakingInstructions.depositSol?.fundingAccount.toString() || - '', - stakingAddress: - stakingInstructions.initialize?.stakePubkey.toString() || - stakingInstructions.depositSol?.stakePool.toString() || - '', - amount: - stakingInstructions.create?.lamports.toString() || stakingInstructions.depositSol?.lamports.toString() || '', - validator: - stakingInstructions.delegate?.votePubkey.toString() || - stakingInstructions.initialize?.authorized.staker.toString() || - stakingInstructions.depositSol?.stakePool.toString() || - '', - isMarinade: stakingInstructionsIsMarinade(stakingInstructions), - isJito: stakingInstructionsIsJito(stakingInstructions), - ...(stakingInstructions.depositSol && stakingInstructionsIsJito(stakingInstructions) - ? { - jitoParams: { - stakePoolData: { - managerFeeAccount: stakingInstructions.depositSol.managerFeeAccount.toString(), - poolMint: stakingInstructions.depositSol.poolMint.toString(), - reserveStake: stakingInstructions.depositSol.reserveStake.toString(), - }, + let stakingActivate: StakingActivate | undefined; + + switch (stakingType) { + case StakingType.JITO: { + assert(isJitoStakingInstructions(stakingInstructions)); + const { depositSol } = stakingInstructions; + stakingActivate = { + type: InstructionBuilderTypes.StakingActivate, + params: { + stakingType, + fromAddress: depositSol.fundingAccount.toString(), + stakingAddress: depositSol.stakePool.toString(), + amount: depositSol.lamports.toString(), + validator: depositSol.stakePool.toString(), + extraParams: { + stakePoolData: { + managerFeeAccount: depositSol.managerFeeAccount.toString(), + poolMint: depositSol.poolMint.toString(), + reserveStake: depositSol.reserveStake.toString(), }, - } - : {}), - }, - }; + }, + }, + }; + break; + } + + case StakingType.MARINADE: { + assert(isMarinadeStakingInstructions(stakingInstructions)); + const { create, initialize } = stakingInstructions; + stakingActivate = { + type: InstructionBuilderTypes.StakingActivate, + params: { + stakingType, + fromAddress: create.fromPubkey.toString(), + stakingAddress: initialize.stakePubkey.toString(), + amount: create.lamports.toString(), + validator: initialize.authorized.staker.toString(), + }, + }; + break; + } + + case StakingType.NATIVE: { + assert(isNativeStakingInstructions(stakingInstructions)); + const { create, initialize, delegate } = stakingInstructions; + stakingActivate = { + type: InstructionBuilderTypes.StakingActivate, + params: { + stakingType, + fromAddress: create.fromPubkey.toString(), + stakingAddress: initialize.stakePubkey.toString(), + amount: create.lamports.toString(), + validator: delegate.votePubkey.toString(), + }, + }; + break; + } + + default: { + const unreachable: never = stakingType; + throw new Error(`Unknown staking type ${unreachable}`); + } + } + instructionData.push(stakingActivate); return instructionData; @@ -481,31 +545,67 @@ function parseStakingDelegateInstructions(instructions: TransactionInstruction[] return instructionData; } -interface StakingInstructions { - create?: CreateAccountParams; - initialize?: InitializeStakeParams; - delegate?: DelegateStakeParams; - authorize?: AuthorizeStakeParams[]; - depositSol?: DepositSolParams; -} - function validateStakingInstructions(stakingInstructions: StakingInstructions) { - if (stakingInstructionsIsJito(stakingInstructions)) { - if (!stakingInstructions.depositSol) { - throw new NotSupported('Invalid staking activate transaction, missing deposit sol instruction'); - } - } else { - if (!stakingInstructions.create) { - throw new NotSupported('Invalid staking activate transaction, missing create stake account instruction'); - } - if (!stakingInstructions.delegate && !stakingInstructions.initialize) { - throw new NotSupported( - 'Invalid staking activate transaction, missing initialize stake account/delegate instruction' - ); - } + if (stakingInstructions.delegate === undefined && stakingInstructions.depositSol !== undefined) { + return; + } + + if (!stakingInstructions.create) { + throw new NotSupported('Invalid staking activate transaction, missing create stake account instruction'); + } + + if (!stakingInstructions.delegate && !stakingInstructions.initialize) { + throw new NotSupported( + 'Invalid staking activate transaction, missing initialize stake account/delegate instruction' + ); } } +type UnstakingInstructions = { + allocate?: AllocateParams; + assign?: AssignParams; + split?: SplitStakeParams; + deactivate?: DeactivateStakeParams; + transfer?: DecodedTransferInstruction; + withdrawStake?: WithdrawStakeParams; +}; + +type JitoUnstakingInstructions = UnstakingInstructions & { + withdrawStake: NonNullable; +}; + +function isJitoUnstakingInstructions(ui: UnstakingInstructions): ui is JitoUnstakingInstructions { + return ui.withdrawStake !== undefined; +} + +type MarinadeUnstakingInstructions = UnstakingInstructions & { + transfer: NonNullable; +}; + +function isMarinadeUnstakingInstructions(ui: UnstakingInstructions): ui is MarinadeUnstakingInstructions { + return ui.transfer !== undefined && ui.deactivate === undefined; +} + +type NativeUnstakingInstructions = UnstakingInstructions & { + deactivate: NonNullable; + split: UnstakingInstructions['split']; +}; + +function isNativeUnstakingInstructions(ui: UnstakingInstructions): ui is NativeUnstakingInstructions { + return ui.deactivate !== undefined; +} + +function getStakingTypeFromUnstakingInstructions(ui: UnstakingInstructions): StakingType { + const isJito = isJitoUnstakingInstructions(ui); + const isMarinade = isMarinadeUnstakingInstructions(ui); + const isNative = isNativeUnstakingInstructions(ui); + assert([isJito, isMarinade, isNative].filter((x) => x).length === 1, 'StakingType is ambiguous'); + if (isJito) return StakingType.JITO; + if (isMarinade) return StakingType.MARINADE; + if (isNative) return StakingType.NATIVE; + assert(false, 'No StakingType found'); +} + /** * Parses Solana instructions to create deactivate stake tx instructions params. Supports full stake * account deactivation and partial stake account deactivation. @@ -634,65 +734,85 @@ function parseStakingDeactivateInstructions( for (const unstakingInstruction of unstakingInstructions) { validateUnstakingInstructions(unstakingInstruction); - const isMarinade = - unstakingInstruction.deactivate === undefined && unstakingInstruction.withdrawStake === undefined; - const stakingDeactivate: StakingDeactivate = { - type: InstructionBuilderTypes.StakingDeactivate, - params: { - fromAddress: - unstakingInstruction.deactivate?.authorizedPubkey.toString() || - unstakingInstruction.withdrawStake?.destinationStakeAuthority.toString() || - '', - stakingAddress: - unstakingInstruction.split?.stakePubkey.toString() || - unstakingInstruction.deactivate?.stakePubkey.toString() || - unstakingInstruction.withdrawStake?.stakePool.toString() || - '', - amount: - unstakingInstruction.split?.lamports.toString() || unstakingInstruction.withdrawStake?.poolTokens.toString(), - unstakingAddress: - unstakingInstruction.split?.splitStakePubkey.toString() || - unstakingInstruction.withdrawStake?.destinationStake.toString(), - isMarinade: isMarinade, - recipients: isMarinade - ? [ - { - address: unstakingInstruction.transfer?.toPubkey.toString() || '', - amount: unstakingInstruction.transfer?.lamports.toString() || '', + const stakingType = getStakingTypeFromUnstakingInstructions(unstakingInstruction); + + let stakingDeactivate: StakingDeactivate | undefined; + + switch (stakingType) { + case StakingType.JITO: { + assert(isJitoUnstakingInstructions(unstakingInstruction)); + const { withdrawStake } = unstakingInstruction; + stakingDeactivate = { + type: InstructionBuilderTypes.StakingDeactivate, + params: { + stakingType, + fromAddress: withdrawStake.destinationStakeAuthority.toString(), + stakingAddress: withdrawStake.stakePool.toString(), + amount: withdrawStake.poolTokens.toString(), + unstakingAddress: withdrawStake.destinationStake.toString(), + extraParams: { + stakePoolData: { + managerFeeAccount: withdrawStake.managerFeeAccount.toString(), + poolMint: withdrawStake.poolMint.toString(), + validatorListAccount: withdrawStake.validatorList.toString(), }, - ] - : undefined, - ...(unstakingInstruction.withdrawStake !== undefined - ? { - isJito: unstakingInstruction.withdrawStake !== undefined, - jitoParams: unstakingInstruction.withdrawStake && { - stakePoolData: { - managerFeeAccount: unstakingInstruction.withdrawStake.managerFeeAccount.toString(), - poolMint: unstakingInstruction.withdrawStake.poolMint.toString(), - validatorList: unstakingInstruction.withdrawStake.validatorList.toString(), - }, - validatorAddress: unstakingInstruction.withdrawStake.validatorStake.toString(), - transferAuthorityAddress: unstakingInstruction.withdrawStake.sourceTransferAuthority.toString(), + validatorAddress: withdrawStake.validatorStake.toString(), + transferAuthorityAddress: withdrawStake.sourceTransferAuthority.toString(), + }, + }, + }; + break; + } + + case StakingType.MARINADE: { + assert(isMarinadeUnstakingInstructions(unstakingInstruction)); + const { transfer } = unstakingInstruction; + + stakingDeactivate = { + type: InstructionBuilderTypes.StakingDeactivate, + params: { + stakingType, + fromAddress: '', + stakingAddress: '', + recipients: [ + { + address: transfer.toPubkey.toString() || '', + amount: transfer.lamports.toString() || '', }, - } - : {}), - }, - }; + ], + }, + }; + break; + } + + case StakingType.NATIVE: { + assert(isNativeUnstakingInstructions(unstakingInstruction)); + const { deactivate, split } = unstakingInstruction; + stakingDeactivate = { + type: InstructionBuilderTypes.StakingDeactivate, + params: { + stakingType, + fromAddress: deactivate.authorizedPubkey.toString() || '', + stakingAddress: split?.stakePubkey.toString() || deactivate.stakePubkey.toString(), + amount: split?.lamports.toString(), + unstakingAddress: split?.splitStakePubkey.toString(), + }, + }; + break; + } + + default: { + const unreachable: never = stakingType; + throw new Error(`Unknown staking type ${unreachable}`); + } + } + instructionData.push(stakingDeactivate); } return instructionData; } -interface UnstakingInstructions { - allocate?: AllocateParams; - assign?: AssignParams; - split?: SplitStakeParams; - deactivate?: DeactivateStakeParams; - transfer?: DecodedTransferInstruction; - withdrawStake?: WithdrawStakeParams; -} - function validateUnstakingInstructions(unstakingInstructions: UnstakingInstructions) { // Cases where exactly one field should be present const unstakingInstructionsKeys: (keyof UnstakingInstructions)[] = [ diff --git a/modules/sdk-coin-sol/src/lib/jitoStakePoolOperations.ts b/modules/sdk-coin-sol/src/lib/jitoStakePoolOperations.ts index 9b09d3a3db..e3f89a61a0 100644 --- a/modules/sdk-coin-sol/src/lib/jitoStakePoolOperations.ts +++ b/modules/sdk-coin-sol/src/lib/jitoStakePoolOperations.ts @@ -47,7 +47,7 @@ export interface StakePoolData { staker: string; stakeDepositAuthority: string; stakeWithdrawBumpSeed: number; - validatorList: string; + validatorListAccount: string; reserveStake: string; poolMint: string; managerFeeAccount: string; @@ -209,7 +209,7 @@ export interface WithdrawStakeInstructionsParams { poolAmount: string; } -export type WithdrawStakeStakePoolData = Pick; +export type WithdrawStakeStakePoolData = Pick; /** * Construct Solana depositSol stake pool instruction from parameters. @@ -234,7 +234,7 @@ export function withdrawStakeInstructions( } = params; const poolMint = new PublicKey(stakePool.poolMint); - const validatorList = new PublicKey(stakePool.validatorList); + const validatorList = new PublicKey(stakePool.validatorListAccount); const managerFeeAccount = new PublicKey(stakePool.managerFeeAccount); const poolTokenAccount = getAssociatedTokenAddressSync(poolMint, tokenOwner); @@ -291,9 +291,9 @@ export function decodeWithdrawStake(instruction: TransactionInstruction): Withdr const validatorList = parseKey(keys[i++], { isSigner: false, isWritable: true }); const withdrawAuthority = parseKey(keys[i++], { isSigner: false, isWritable: false }); const validatorStake = parseKey(keys[i++], { isSigner: false, isWritable: true }); - const destinationStake = keys[i++].pubkey; // parseKey(keys[i++], { isSigner: false, isWritable: true }); - const destinationStakeAuthority = keys[i++].pubkey; // parseKey(keys[i++], { isSigner: false, isWritable: false }); - const sourceTransferAuthority = keys[i++].pubkey; // parseKey(keys[i++], { isSigner: true, isWritable: false }); + const destinationStake = keys[i++].pubkey; + const destinationStakeAuthority = keys[i++].pubkey; + const sourceTransferAuthority = parseKey(keys[i++], { isSigner: true, isWritable: false }); const sourcePoolAccount = parseKey(keys[i++], { isSigner: false, isWritable: true }); const managerFeeAccount = parseKey(keys[i++], { isSigner: false, isWritable: true }); const poolMint = parseKey(keys[i++], { isSigner: false, isWritable: true }); diff --git a/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts b/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts index f9ecb0e5d8..82b393bfba 100644 --- a/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts +++ b/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts @@ -41,6 +41,7 @@ import { SetPriorityFee, CustomInstruction, Approve, + StakingType, } from './iface'; import { getSolTokenFromTokenName, isValidBase64, isValidHex } from './utils'; import { depositSolInstructions, withdrawStakeInstructions } from './jitoStakePoolOperations'; @@ -274,58 +275,68 @@ function createNonceAccountInstruction(data: WalletInit): TransactionInstruction */ function stakingInitializeInstruction(data: StakingActivate): TransactionInstruction[] { const { - params: { fromAddress, stakingAddress, amount, validator, isMarinade, isJito, jitoParams }, + params: { fromAddress, stakingAddress, amount, validator, stakingType, extraParams }, } = data; assert(fromAddress, 'Missing fromAddress param'); assert(stakingAddress, 'Missing stakingAddress param'); assert(amount, 'Missing amount param'); assert(validator, 'Missing validator param'); - assert(isMarinade !== undefined, 'Missing isMarinade param'); - assert(isJito !== undefined, 'Missing isJito param'); - assert([isMarinade, isJito].filter((x) => x).length <= 1, 'At most one of isMarinade and isJito can be true'); const fromPubkey = new PublicKey(fromAddress); const stakePubkey = new PublicKey(stakingAddress); const validatorPubkey = new PublicKey(validator); const tx = new Transaction(); - if (isJito) { - assert(jitoParams, 'missing jitoParams param'); - - const instructions = depositSolInstructions( - { - stakePoolAddress: stakePubkey, - from: fromPubkey, - lamports: BigInt(amount), - }, - jitoParams.stakePoolData - ); - tx.add(...instructions); - } else if (isMarinade) { - const walletInitStaking = StakeProgram.createAccount({ - fromPubkey, - stakePubkey, - authorized: new Authorized(validatorPubkey, fromPubkey), // staker and withdrawer - lockup: new Lockup(0, 0, fromPubkey), // No minimum epoch to withdraw - lamports: new BigNumber(amount).toNumber(), - }); - tx.add(walletInitStaking); - } else { - const walletInitStaking = StakeProgram.createAccount({ - fromPubkey, - stakePubkey, - authorized: new Authorized(fromPubkey, fromPubkey), // staker and withdrawer - lockup: new Lockup(0, 0, fromPubkey), // No minimum epoch to withdraw - lamports: new BigNumber(amount).toNumber(), - }); - tx.add(walletInitStaking); - - const delegateStaking = StakeProgram.delegate({ - stakePubkey: new PublicKey(stakingAddress), - authorizedPubkey: new PublicKey(fromAddress), - votePubkey: new PublicKey(validator), - }); - tx.add(delegateStaking); + switch (stakingType) { + case StakingType.JITO: { + assert(extraParams !== undefined, 'Missing extraParams param'); + const instructions = depositSolInstructions( + { + stakePoolAddress: stakePubkey, + from: fromPubkey, + lamports: BigInt(amount), + }, + extraParams.stakePoolData + ); + tx.add(...instructions); + break; + } + + case StakingType.MARINADE: { + const walletInitStaking = StakeProgram.createAccount({ + fromPubkey, + stakePubkey, + authorized: new Authorized(validatorPubkey, fromPubkey), // staker and withdrawer + lockup: new Lockup(0, 0, fromPubkey), // No minimum epoch to withdraw + lamports: new BigNumber(amount).toNumber(), + }); + tx.add(walletInitStaking); + break; + } + + case StakingType.NATIVE: { + const walletInitStaking = StakeProgram.createAccount({ + fromPubkey, + stakePubkey, + authorized: new Authorized(fromPubkey, fromPubkey), // staker and withdrawer + lockup: new Lockup(0, 0, fromPubkey), // No minimum epoch to withdraw + lamports: new BigNumber(amount).toNumber(), + }); + tx.add(walletInitStaking); + + const delegateStaking = StakeProgram.delegate({ + stakePubkey: new PublicKey(stakingAddress), + authorizedPubkey: new PublicKey(fromAddress), + votePubkey: new PublicKey(validator), + }); + tx.add(delegateStaking); + break; + } + + default: { + const unreachable: never = stakingType; + throw new Error(`Unknown staking type ${unreachable}`); + } } return tx.instructions; @@ -339,88 +350,100 @@ function stakingInitializeInstruction(data: StakingActivate): TransactionInstruc */ function stakingDeactivateInstruction(data: StakingDeactivate): TransactionInstruction[] { const { - params: { fromAddress, stakingAddress, amount, unstakingAddress, isMarinade, isJito, recipients, jitoParams }, + params: { fromAddress, stakingAddress, amount, unstakingAddress, recipients, stakingType, extraParams }, } = data; assert(fromAddress, 'Missing fromAddress param'); - if (!isMarinade) { - assert(stakingAddress, 'Missing stakingAddress param'); - } - assert([isMarinade, isJito].filter((x) => x).length <= 1, 'At most one of isMarinade and isJito can be true'); - - if (isJito) { - assert(unstakingAddress, 'Missing unstakingAddress param'); - assert(amount, 'Missing amount param'); - assert(jitoParams, 'Missing jitoParams param'); - - const tx = new Transaction(); - tx.add( - ...withdrawStakeInstructions( - { - stakePoolAddress: new PublicKey(stakingAddress), - tokenOwner: new PublicKey(fromAddress), - destinationStakeAccount: new PublicKey(unstakingAddress), - validatorAddress: new PublicKey(jitoParams.validatorAddress), - transferAuthority: new PublicKey(jitoParams.transferAuthorityAddress), - poolAmount: amount, - }, - jitoParams.stakePoolData - ) - ); - return tx.instructions; - } else if (isMarinade) { - assert(recipients, 'Missing recipients param'); - - const tx = new Transaction(); - const toPubkeyAddress = new PublicKey(recipients[0].address || ''); - const transferInstruction = SystemProgram.transfer({ - fromPubkey: new PublicKey(fromAddress), - toPubkey: toPubkeyAddress, - lamports: parseInt(recipients[0].amount, 10), - }); - - tx.add(transferInstruction); - return tx.instructions; - } else if (data.params.amount && data.params.unstakingAddress) { - const tx = new Transaction(); - const unstakingAddress = new PublicKey(data.params.unstakingAddress); - - const allocateAccount = SystemProgram.allocate({ - accountPubkey: unstakingAddress, - space: StakeProgram.space, - }); - tx.add(allocateAccount); - - const assignAccount = SystemProgram.assign({ - accountPubkey: unstakingAddress, - programId: StakeProgram.programId, - }); - tx.add(assignAccount); - - const splitStake = StakeProgram.split( - { - stakePubkey: new PublicKey(stakingAddress), - authorizedPubkey: new PublicKey(fromAddress), - splitStakePubkey: unstakingAddress, - lamports: new BigNumber(data.params.amount).toNumber(), - }, - 0 - ); - tx.add(splitStake.instructions[1]); - - const deactivateStaking = StakeProgram.deactivate({ - stakePubkey: unstakingAddress, - authorizedPubkey: new PublicKey(fromAddress), - }); - tx.add(deactivateStaking); - - return tx.instructions; - } else { - const deactivateStaking = StakeProgram.deactivate({ - stakePubkey: new PublicKey(stakingAddress), - authorizedPubkey: new PublicKey(fromAddress), - }); - return deactivateStaking.instructions; + switch (stakingType) { + case StakingType.JITO: { + assert(stakingAddress, 'Missing stakingAddress param'); + assert(unstakingAddress, 'Missing unstakingAddress param'); + assert(amount, 'Missing amount param'); + assert(extraParams, 'Missing extraParams param'); + + const tx = new Transaction(); + tx.add( + ...withdrawStakeInstructions( + { + stakePoolAddress: new PublicKey(stakingAddress), + tokenOwner: new PublicKey(fromAddress), + destinationStakeAccount: new PublicKey(unstakingAddress), + validatorAddress: new PublicKey(extraParams.validatorAddress), + transferAuthority: new PublicKey(extraParams.transferAuthorityAddress), + poolAmount: amount, + }, + extraParams.stakePoolData + ) + ); + return tx.instructions; + } + + case StakingType.MARINADE: { + assert(recipients, 'Missing recipients param'); + + const tx = new Transaction(); + const toPubkeyAddress = new PublicKey(recipients[0].address || ''); + const transferInstruction = SystemProgram.transfer({ + fromPubkey: new PublicKey(fromAddress), + toPubkey: toPubkeyAddress, + lamports: parseInt(recipients[0].amount, 10), + }); + + tx.add(transferInstruction); + return tx.instructions; + } + + case StakingType.NATIVE: { + assert(stakingAddress, 'Missing stakingAddress param'); + + if (data.params.amount && data.params.unstakingAddress) { + const tx = new Transaction(); + const unstakingAddress = new PublicKey(data.params.unstakingAddress); + + const allocateAccount = SystemProgram.allocate({ + accountPubkey: unstakingAddress, + space: StakeProgram.space, + }); + tx.add(allocateAccount); + + const assignAccount = SystemProgram.assign({ + accountPubkey: unstakingAddress, + programId: StakeProgram.programId, + }); + tx.add(assignAccount); + + const splitStake = StakeProgram.split( + { + stakePubkey: new PublicKey(stakingAddress), + authorizedPubkey: new PublicKey(fromAddress), + splitStakePubkey: unstakingAddress, + lamports: new BigNumber(data.params.amount).toNumber(), + }, + 0 + ); + tx.add(splitStake.instructions[1]); + + const deactivateStaking = StakeProgram.deactivate({ + stakePubkey: unstakingAddress, + authorizedPubkey: new PublicKey(fromAddress), + }); + tx.add(deactivateStaking); + + return tx.instructions; + } else { + const deactivateStaking = StakeProgram.deactivate({ + stakePubkey: new PublicKey(stakingAddress), + authorizedPubkey: new PublicKey(fromAddress), + }); + + return deactivateStaking.instructions; + } + } + + default: { + const unreachable: never = stakingType; + throw new Error(`Unknown staking type ${unreachable}`); + } } } diff --git a/modules/sdk-coin-sol/src/lib/stakingActivateBuilder.ts b/modules/sdk-coin-sol/src/lib/stakingActivateBuilder.ts index 3bc7c2b48f..f2783f8d90 100644 --- a/modules/sdk-coin-sol/src/lib/stakingActivateBuilder.ts +++ b/modules/sdk-coin-sol/src/lib/stakingActivateBuilder.ts @@ -5,16 +5,15 @@ import { TransactionBuilder } from './transactionBuilder'; import { InstructionBuilderTypes } from './constants'; import assert from 'assert'; -import { StakingActivate } from './iface'; +import { StakingActivate, StakingActivateExtraParams, StakingType } from './iface'; import { isValidStakingAmount, validateAddress } from './utils'; export class StakingActivateBuilder extends TransactionBuilder { protected _amount: string; protected _stakingAddress: string; protected _validator: string; - protected _isMarinade = false; - protected _isJito = false; - protected _jitoParams?: StakingActivate['params']['jitoParams']; + protected _stakingType: StakingType = StakingType.NATIVE; + protected _extraParams?: StakingActivateExtraParams; constructor(_coinConfig: Readonly) { super(_coinConfig); @@ -34,9 +33,8 @@ export class StakingActivateBuilder extends TransactionBuilder { this.stakingAddress(activateInstruction.params.stakingAddress); this.amount(activateInstruction.params.amount); this.validator(activateInstruction.params.validator); - this.isMarinade(activateInstruction.params.isMarinade ?? false); - this.isJito(activateInstruction.params.isJito ?? false); - this.jitoParams(activateInstruction.params.jitoParams); + this.stakingType(activateInstruction.params.stakingType); + this.extraParams(activateInstruction.params.extraParams); } } } @@ -84,34 +82,24 @@ export class StakingActivateBuilder extends TransactionBuilder { } /** - * Set isMarinade flag - * @param {boolean} flag - true if the transaction is for Marinade, false by default if not set - * @returns {StakingActivateBuilder} This staking builder - */ - isMarinade(flag: boolean): this { - this._isMarinade = flag; - return this; - } - - /** - * Set isJito flag - * @param {boolean} flag - true if the transaction is for Jito, false by default if not set - * @returns {StakingActivateBuilder} This staking builder + * Set staking type. + * + * @param {StakingType} stakingType a staking type. + * @returns {StakingActivateBuilder} This staking builder. */ - isJito(flag: boolean): this { - this._isJito = flag; - // this._coinConfig.network.type === NetworkType.TESTNET + stakingType(stakingType: StakingType): this { + this._stakingType = stakingType; return this; } /** - * Set parameters specific to Jito staking. + * Set parameters specific to a staking type. * - * @param {string} jitoParams parameters specific to Jito staking. + * @param {StakingActivateExtraParams} extraParams parameters specific to a staking type. * @returns {StakingActivateBuilder} This staking builder. */ - jitoParams(jitoParams: StakingActivate['params']['jitoParams']): this { - this._jitoParams = jitoParams; + extraParams(extraParams?: StakingActivateExtraParams): this { + this._extraParams = extraParams; return this; } @@ -121,12 +109,6 @@ export class StakingActivateBuilder extends TransactionBuilder { assert(this._stakingAddress, 'Staking Address must be set before building the transaction'); assert(this._validator, 'Validator must be set before building the transaction'); assert(this._amount, 'Amount must be set before building the transaction'); - assert(this._isMarinade !== undefined, 'isMarinade must be set before building the transaction'); - assert(this._isJito !== undefined, 'isJito must be set before building the transaction'); - assert( - [this._isMarinade, this._isJito].filter((x) => x).length <= 1, - 'At most one of isMarinade and isJito can be true' - ); if (this._sender === this._stakingAddress) { throw new BuildTransactionError('Sender address cannot be the same as the Staking address'); @@ -139,9 +121,8 @@ export class StakingActivateBuilder extends TransactionBuilder { stakingAddress: this._stakingAddress, amount: this._amount, validator: this._validator, - isMarinade: this._isMarinade, - isJito: this._isJito, - jitoParams: this._jitoParams, + stakingType: this._stakingType, + extraParams: this._extraParams, }, }; this._instructionsData = [stakingAccountData]; diff --git a/modules/sdk-coin-sol/src/lib/stakingDeactivateBuilder.ts b/modules/sdk-coin-sol/src/lib/stakingDeactivateBuilder.ts index 694df516e7..77f38fb927 100644 --- a/modules/sdk-coin-sol/src/lib/stakingDeactivateBuilder.ts +++ b/modules/sdk-coin-sol/src/lib/stakingDeactivateBuilder.ts @@ -3,7 +3,7 @@ import assert from 'assert'; import { BuildTransactionError, Recipient, TransactionType } from '@bitgo/sdk-core'; import { InstructionBuilderTypes, STAKE_ACCOUNT_RENT_EXEMPT_AMOUNT } from './constants'; -import { StakingDeactivate, Transfer } from './iface'; +import { StakingDeactivate, StakingDeactivateExtraParams, StakingType, Transfer } from './iface'; import { Transaction } from './transaction'; import { TransactionBuilder } from './transactionBuilder'; import { isValidStakingAmount, validateAddress } from './utils'; @@ -13,10 +13,9 @@ export class StakingDeactivateBuilder extends TransactionBuilder { protected _stakingAddresses: string[]; protected _amount?: string; protected _unstakingAddress: string; - protected _isMarinade = false; - protected _isJito = false; protected _recipients: Recipient[]; - protected _jitoParams?: StakingDeactivate['params']['jitoParams']; + protected _stakingType: StakingType = StakingType.NATIVE; + protected _extraParams?: StakingDeactivateExtraParams; constructor(_coinConfig: Readonly) { super(_coinConfig); @@ -37,7 +36,7 @@ export class StakingDeactivateBuilder extends TransactionBuilder { stakingAddresses.push(deactivateInstruction.params.stakingAddress); // Marinade staking also cares about sender. - if (!deactivateInstruction.params.isMarinade) { + if (deactivateInstruction.params.stakingType !== StakingType.MARINADE) { this.sender(deactivateInstruction.params.fromAddress); } @@ -48,24 +47,21 @@ export class StakingDeactivateBuilder extends TransactionBuilder { if (deactivateInstruction.params.unstakingAddress !== undefined) { this.unstakingAddress(deactivateInstruction.params.unstakingAddress); } - if (deactivateInstruction.params.isMarinade !== undefined) { - this.isMarinade(deactivateInstruction.params.isMarinade); - } - if (deactivateInstruction.params.isJito !== undefined) { - this.isJito(deactivateInstruction.params.isJito); - } if (deactivateInstruction.params.recipients !== undefined) { this.recipients(deactivateInstruction.params.recipients); } - if (deactivateInstruction.params.jitoParams !== undefined) { - this.jitoParams(deactivateInstruction.params.jitoParams); + if (deactivateInstruction.params.stakingType !== undefined) { + this.stakingType(deactivateInstruction.params.stakingType); + } + if (deactivateInstruction.params.extraParams !== undefined) { + this.extraParams(deactivateInstruction.params.extraParams); } } } if (stakingAddresses.length > 1) { this.stakingAddresses(stakingAddresses); } else { - if (!this._isMarinade) { + if (this._stakingType !== StakingType.MARINADE) { this.stakingAddress(stakingAddresses[0]); } } @@ -145,40 +141,30 @@ export class StakingDeactivateBuilder extends TransactionBuilder { } /** - * Set isMarinade flag - * @param {boolean} flag - true if the transaction is for Marinade, false by default if not set - * @returns {StakingDectivateBuilder} This staking builder - */ - isMarinade(flag: boolean): this { - this._isMarinade = flag; - return this; - } - - /** - * Set isJito flag - * @param {boolean} flag - true if the transaction is for Jito, false by default if not set - * @returns {StakingDeactivateBuilder} This staking builder + * Set staking type. + * + * @param {StakingType} stakingType a staking type. + * @returns {StakingDeactivateBuilder} This staking builder. */ - isJito(flag: boolean): this { - this._isJito = flag; + stakingType(stakingType: StakingType): this { + this._stakingType = stakingType; return this; } /** - * Set parameters specific to Jito unstaking. + * Set parameters specific to a staking type. * - * @param {string} jitoParams parameters specific to Jito unstaking. + * @param {StakingDeactivateExtraParams} extraParams parameters specific to a staking type. * @returns {StakingDeactivateBuilder} This staking builder. */ - jitoParams(jitoParams: StakingDeactivate['params']['jitoParams']): this { - this._jitoParams = jitoParams; + extraParams(extraParams?: StakingDeactivateExtraParams): this { + this._extraParams = extraParams; return this; } /** @inheritdoc */ protected async buildImplementation(): Promise { assert(this._sender, 'Sender must be set before building the transaction'); - assert(this._isMarinade !== undefined, 'isMarinade must be set before building the transaction'); if (this._stakingAddresses && this._stakingAddresses.length > 0) { this._instructionsData = []; @@ -188,12 +174,13 @@ export class StakingDeactivateBuilder extends TransactionBuilder { params: { fromAddress: this._sender, stakingAddress: stakingAddress, + stakingType: StakingType.NATIVE, }, }; this._instructionsData.push(stakingDeactivateData); } } else { - if (!this._isMarinade && !this._isJito) { + if (this._stakingType === StakingType.NATIVE) { // we don't need stakingAddress in marinade staking deactivate txn assert(this._stakingAddress, 'Staking address must be set before building the transaction'); } @@ -202,7 +189,7 @@ export class StakingDeactivateBuilder extends TransactionBuilder { throw new BuildTransactionError('Sender address cannot be the same as the Staking address'); } - if (this._amount && !this._isMarinade && !this._isJito) { + if (this._stakingType === StakingType.NATIVE && this._amount) { assert( this._unstakingAddress, 'When partially unstaking the unstaking address must be set before building the transaction' @@ -210,7 +197,7 @@ export class StakingDeactivateBuilder extends TransactionBuilder { } this._instructionsData = []; - if (this._unstakingAddress && !this._isMarinade && !this._isJito) { + if (this._stakingType === StakingType.NATIVE && this._unstakingAddress) { assert( this._amount, 'If an unstaking address is given then a partial amount to unstake must also be set before building the transaction' @@ -233,10 +220,9 @@ export class StakingDeactivateBuilder extends TransactionBuilder { stakingAddress: this._stakingAddress, amount: this._amount, unstakingAddress: this._unstakingAddress, - isMarinade: this._isMarinade, - isJito: this._isJito, recipients: this._recipients, - jitoParams: this._jitoParams, + stakingType: this._stakingType, + extraParams: this._extraParams, }, }; this._instructionsData.push(stakingDeactivateData); diff --git a/modules/sdk-coin-sol/test/unit/instructionParamsFactory.staking.ts b/modules/sdk-coin-sol/test/unit/instructionParamsFactory.staking.ts index 1f314cbe4a..6435fe3fac 100644 --- a/modules/sdk-coin-sol/test/unit/instructionParamsFactory.staking.ts +++ b/modules/sdk-coin-sol/test/unit/instructionParamsFactory.staking.ts @@ -2,7 +2,14 @@ import should from 'should'; import * as testData from '../resources/sol'; import { instructionParamsFactory } from '../../src/lib/instructionParamsFactory'; import { TransactionType } from '@bitgo/sdk-core'; -import { InstructionParams, Nonce, StakingActivate, StakingDeactivate, StakingWithdraw } from '../../src/lib/iface'; +import { + InstructionParams, + Nonce, + StakingActivate, + StakingDeactivate, + StakingType, + StakingWithdraw, +} from '../../src/lib/iface'; import { InstructionBuilderTypes, MEMO_PROGRAM_PK, STAKE_ACCOUNT_RENT_EXEMPT_AMOUNT } from '../../src/lib/constants'; import { Keypair as SolKeypair, @@ -66,8 +73,7 @@ describe('Instruction Parser Staking Tests: ', function () { stakingAddress: stakingAccount.toString(), validator: validator.toString(), amount, - isMarinade: false, - isJito: false, + stakingType: StakingType.NATIVE, }, }; @@ -137,8 +143,7 @@ describe('Instruction Parser Staking Tests: ', function () { stakingAddress: stakingAccount.toString(), validator: validator.toString(), amount, - isMarinade: false, - isJito: false, + stakingType: StakingType.NATIVE, }, }; @@ -190,8 +195,7 @@ describe('Instruction Parser Staking Tests: ', function () { stakingAddress: stakingAccount.toString(), validator: validator.toString(), amount, - isMarinade: false, - isJito: false, + stakingType: StakingType.NATIVE, }, }; @@ -238,8 +242,7 @@ describe('Instruction Parser Staking Tests: ', function () { stakingAddress: stakingAccount.toString(), validator: validator.toString(), amount, - isMarinade: false, - isJito: false, + stakingType: StakingType.NATIVE, }, }; @@ -326,8 +329,7 @@ describe('Instruction Parser Staking Tests: ', function () { stakingAddress: stakingAccount.toString(), amount: undefined, unstakingAddress: undefined, - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }; @@ -378,8 +380,7 @@ describe('Instruction Parser Staking Tests: ', function () { stakingAddress: stakingAccount.toString(), amount: undefined, unstakingAddress: undefined, - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }; @@ -412,8 +413,7 @@ describe('Instruction Parser Staking Tests: ', function () { stakingAddress: stakingAccount.toString(), amount: undefined, unstakingAddress: undefined, - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }; @@ -460,8 +460,7 @@ describe('Instruction Parser Staking Tests: ', function () { stakingAddress: stakingAccount.toString(), amount: undefined, unstakingAddress: undefined, - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }; @@ -1033,8 +1032,7 @@ describe('Instruction Parser Staking Tests: ', function () { stakingAddress: stakingAccount.toString(), amount: '100000', unstakingAddress: splitStakeAccount.toString(), - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }; diff --git a/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingActivateBuilder.ts b/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingActivateBuilder.ts index 86908342fc..acdd3ca03e 100644 --- a/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingActivateBuilder.ts +++ b/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingActivateBuilder.ts @@ -5,6 +5,7 @@ import { getBuilderFactory } from '../getBuilderFactory'; import { KeyPair, Utils, Transaction } from '../../../src'; import { coins } from '@bitgo/statics'; import { JITO_STAKE_POOL_ADDRESS, JITOSOL_MINT_ADDRESS } from '../../../src/lib/constants'; +import { StakingType } from '../../../src/lib/iface'; describe('Sol Staking Activate Builder', () => { const factory = getBuilderFactory('tsol'); @@ -63,7 +64,7 @@ describe('Sol Staking Activate Builder', () => { .sender(wallet.pub) .stakingAddress(stakeAccount.pub) .validator(validator.pub) - .isMarinade(true) + .stakingType(StakingType.MARINADE) .nonce(recentBlockHash); txBuilder.sign({ key: wallet.prv }); txBuilder.sign({ key: stakeAccount.prv }); @@ -77,8 +78,7 @@ describe('Sol Staking Activate Builder', () => { stakingAddress: stakeAccount.pub, amount: amount, validator: validator.pub, - isMarinade: true, - isJito: false, + stakingType: StakingType.MARINADE, }, }, ]); @@ -105,14 +105,14 @@ describe('Sol Staking Activate Builder', () => { .sender(wallet.pub) .stakingAddress(JITO_STAKE_POOL_ADDRESS) .validator(JITO_STAKE_POOL_ADDRESS) - .jitoParams({ + .stakingType(StakingType.JITO) + .extraParams({ stakePoolData: { managerFeeAccount: testData.JITO_STAKE_POOL_DATA_PARSED.managerFeeAccount.toString(), poolMint: testData.JITO_STAKE_POOL_DATA_PARSED.poolMint.toString(), reserveStake: testData.JITO_STAKE_POOL_DATA_PARSED.reserveStake.toString(), }, }) - .isJito(true) .nonce(recentBlockHash); txBuilder.sign({ key: wallet.prv }); const tx = await txBuilder.build(); @@ -135,9 +135,8 @@ describe('Sol Staking Activate Builder', () => { stakingAddress: JITO_STAKE_POOL_ADDRESS, amount: amount, validator: JITO_STAKE_POOL_ADDRESS, - isMarinade: false, - isJito: true, - jitoParams: { + stakingType: StakingType.JITO, + extraParams: { stakePoolData: { managerFeeAccount: testData.JITO_STAKE_POOL_DATA_PARSED.managerFeeAccount.toString(), poolMint: testData.JITO_STAKE_POOL_DATA_PARSED.poolMint.toString(), @@ -200,7 +199,7 @@ describe('Sol Staking Activate Builder', () => { .stakingAddress(stakeAccount.pub) .validator(validator.pub) .memo('test memo') - .isMarinade(true) + .stakingType(StakingType.MARINADE) .nonce(recentBlockHash); txBuilder.sign({ key: wallet.prv }); txBuilder.sign({ key: stakeAccount.prv }); @@ -220,8 +219,7 @@ describe('Sol Staking Activate Builder', () => { stakingAddress: stakeAccount.pub, amount: amount, validator: validator.pub, - isMarinade: true, - isJito: false, + stakingType: StakingType.MARINADE, }, }, ]); @@ -270,7 +268,7 @@ describe('Sol Staking Activate Builder', () => { .sender(wallet.pub) .stakingAddress(stakeAccount.pub) .validator(validator.pub) - .isMarinade(true) + .stakingType(StakingType.MARINADE) .nonce(recentBlockHash); const tx = await txBuilder.build(); const txJson = tx.toJson(); @@ -282,8 +280,7 @@ describe('Sol Staking Activate Builder', () => { stakingAddress: stakeAccount.pub, amount: amount, validator: validator.pub, - isMarinade: true, - isJito: false, + stakingType: StakingType.MARINADE, }, }, ]); @@ -329,7 +326,7 @@ describe('Sol Staking Activate Builder', () => { .stakingAddress(stakeAccount.pub) .validator(validator.pub) .memo('test memo') - .isMarinade(true) + .stakingType(StakingType.MARINADE) .nonce(recentBlockHash); const tx = await txBuilder.build(); const txJson = tx.toJson(); @@ -347,8 +344,7 @@ describe('Sol Staking Activate Builder', () => { stakingAddress: stakeAccount.pub, amount: amount, validator: validator.pub, - isMarinade: true, - isJito: false, + stakingType: StakingType.MARINADE, }, }, ]); diff --git a/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingDeactivateBuilder.ts b/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingDeactivateBuilder.ts index 665489bdfa..a929a37dc5 100644 --- a/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingDeactivateBuilder.ts +++ b/modules/sdk-coin-sol/test/unit/transactionBuilder/stakingDeactivateBuilder.ts @@ -6,6 +6,7 @@ import * as testData from '../../resources/sol'; import { Recipient, TransactionType } from '@bitgo/sdk-core'; import * as bs58 from 'bs58'; import { JITO_STAKE_POOL_ADDRESS } from '../../../src/lib/constants'; +import { StakingType } from '../../../src/lib/iface'; describe('Sol Staking Deactivate Builder', () => { const factory = getBuilderFactory('tsol'); @@ -42,8 +43,7 @@ describe('Sol Staking Deactivate Builder', () => { stakingAddress: stakeAccount.pub, amount: undefined, unstakingAddress: undefined, - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }, ]); @@ -71,7 +71,7 @@ describe('Sol Staking Deactivate Builder', () => { .sender(wallet.pub) .stakingAddress(stakeAccount.pub) .nonce(recentBlockHash) - .isMarinade(true) + .stakingType(StakingType.MARINADE) .memo(marinadeMemo) .recipients(marinadeRecipientsObject); const txUnsigned = await txBuilder.build(); @@ -92,9 +92,7 @@ describe('Sol Staking Deactivate Builder', () => { params: { fromAddress: '', stakingAddress: '', - amount: undefined, - unstakingAddress: undefined, - isMarinade: true, + stakingType: StakingType.MARINADE, recipients: marinadeRecipientsObject, }, }, @@ -112,7 +110,7 @@ describe('Sol Staking Deactivate Builder', () => { .sender(wallet.pub) .stakingAddress(stakeAccount.pub) .nonce(recentBlockHash) - .isMarinade(true) + .stakingType(StakingType.MARINADE) .memo(marinadeMemo) .recipients(marinadeRecipientsObject); await txBuilder2.addSignature({ pub: wallet.pub }, Buffer.from(bs58.decode(signed))); @@ -128,16 +126,16 @@ describe('Sol Staking Deactivate Builder', () => { const transferAuthority = new KeyPair(testData.splitStakeAccount).getKeys(); txBuilder .sender(wallet.pub) - .isJito(true) .stakingAddress(JITO_STAKE_POOL_ADDRESS) .unstakingAddress(stakeAccount.pub) - .jitoParams({ + .stakingType(StakingType.JITO) + .extraParams({ validatorAddress: testData.JITO_STAKE_POOL_VALIDATOR_ADDRESS, transferAuthorityAddress: transferAuthority.pub, stakePoolData: { managerFeeAccount: testData.JITO_STAKE_POOL_DATA_PARSED.managerFeeAccount.toString(), poolMint: testData.JITO_STAKE_POOL_DATA_PARSED.poolMint.toString(), - validatorList: testData.JITO_STAKE_POOL_DATA_PARSED.validatorList.toString(), + validatorListAccount: testData.JITO_STAKE_POOL_DATA_PARSED.validatorList.toString(), }, }) .amount('1000') @@ -155,19 +153,17 @@ describe('Sol Staking Deactivate Builder', () => { type: 'Deactivate', params: { fromAddress: wallet.pub, - isMarinade: false, - isJito: true, stakingAddress: JITO_STAKE_POOL_ADDRESS, unstakingAddress: stakeAccount.pub, amount: '1000', - recipients: undefined, - jitoParams: { + stakingType: StakingType.JITO, + extraParams: { validatorAddress: testData.JITO_STAKE_POOL_VALIDATOR_ADDRESS, transferAuthorityAddress: transferAuthority.pub, stakePoolData: { managerFeeAccount: testData.JITO_STAKE_POOL_DATA_PARSED.managerFeeAccount.toString(), poolMint: testData.JITO_STAKE_POOL_DATA_PARSED.poolMint.toString(), - validatorList: testData.JITO_STAKE_POOL_DATA_PARSED.validatorList.toString(), + validatorListAccount: testData.JITO_STAKE_POOL_DATA_PARSED.validatorList.toString(), }, }, }, @@ -197,8 +193,7 @@ describe('Sol Staking Deactivate Builder', () => { stakingAddress: stakeAccount.pub, amount: undefined, unstakingAddress: undefined, - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }, { @@ -208,8 +203,7 @@ describe('Sol Staking Deactivate Builder', () => { stakingAddress: splitAccount.pub, amount: undefined, unstakingAddress: undefined, - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }, ]); @@ -247,8 +241,7 @@ describe('Sol Staking Deactivate Builder', () => { stakingAddress: stakeAccount.pub, amount: undefined, unstakingAddress: undefined, - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }, ]); @@ -318,8 +311,7 @@ describe('Sol Staking Deactivate Builder', () => { stakingAddress: stakeAccount.pub, amount: undefined, unstakingAddress: undefined, - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }, ]); @@ -341,8 +333,7 @@ describe('Sol Staking Deactivate Builder', () => { stakingAddress: stakeAccount.pub, amount: undefined, unstakingAddress: undefined, - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }, ]); @@ -370,8 +361,7 @@ describe('Sol Staking Deactivate Builder', () => { stakingAddress: stakeAccount.pub, amount: undefined, unstakingAddress: undefined, - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }, ]); @@ -413,8 +403,7 @@ describe('Sol Staking Deactivate Builder', () => { stakingAddress: stakeAccount.pub, amount: '100000', unstakingAddress: testData.splitStakeAccount.pub, - isMarinade: false, - recipients: undefined, + stakingType: StakingType.NATIVE, }, }, ]); diff --git a/modules/sdk-coin-sol/test/unit/transactionBuilder/transactionBuilder.ts b/modules/sdk-coin-sol/test/unit/transactionBuilder/transactionBuilder.ts index 70e2fc4b7d..ff4784444c 100644 --- a/modules/sdk-coin-sol/test/unit/transactionBuilder/transactionBuilder.ts +++ b/modules/sdk-coin-sol/test/unit/transactionBuilder/transactionBuilder.ts @@ -7,6 +7,7 @@ import { Eddsa, TransactionType } from '@bitgo/sdk-core'; import * as testData from '../../resources/sol'; import BigNumber from 'bignumber.js'; import { Ed25519Bip32HdTree } from '@bitgo/sdk-lib-mpc'; +import { StakingType } from '../../../src/lib/iface'; describe('Sol Transaction Builder', async () => { let builders; @@ -107,8 +108,7 @@ describe('Sol Transaction Builder', async () => { stakingAddress: '7dRuGFbU2y2kijP6o1LYNzVyz4yf13MooqoionCzv5Za', amount: '300000', validator: 'CyjoLt3kjqB57K7ewCBHmnHq3UgEj3ak6A7m6EsBsuhA', - isMarinade: false, - isJito: false, + stakingType: StakingType.NATIVE, }, }, ]);