Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

- stake_pool: implement js bindings for token metadata instructions #3441

Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 120 additions & 83 deletions stake-pool/js/package-lock.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions stake-pool/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solana/spl-stake-pool",
"version": "0.6.4",
"version": "0.6.8",
"description": "SPL Stake Pool Program JS API",
"scripts": {
"build": "npm run clean && tsc && cross-env NODE_ENV=production rollup -c",
Expand Down Expand Up @@ -90,4 +90,3 @@
"testEnvironment": "node"
}
}

9 changes: 9 additions & 0 deletions stake-pool/js/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Buffer } from 'buffer';
import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';

// Public key that identifies the metadata program.
export const METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');
export const METADATA_MAX_NAME_LENGTH = 32;
export const METADATA_MAX_SYMBOL_LENGTH = 10;
export const METADATA_MAX_URI_LENGTH = 200;

// Public key that identifies the SPL Stake Pool program.
export const STAKE_POOL_PROGRAM_ID = new PublicKey('SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy');

Expand All @@ -13,3 +19,6 @@ export const TRANSIENT_STAKE_SEED_PREFIX = Buffer.from('transient');
// Minimum amount of staked SOL required in a validator stake account to allow
// for merges without a mismatch on credits observed
export const MINIMUM_ACTIVE_STAKE = LAMPORTS_PER_SOL;

// Minimum amount of SOL in the reserve
export const MINIMUM_RESERVE_LAMPORTS = LAMPORTS_PER_SOL;
102 changes: 86 additions & 16 deletions stake-pool/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,8 @@ export async function withdrawStake(
voteAccountAddress?: PublicKey,
stakeReceiver?: PublicKey,
poolTokenAccount?: PublicKey,
validatorComparator?: (_a: ValidatorAccount, _b: ValidatorAccount) => number,
validatorComparator?: (a: ValidatorAccount, b: ValidatorAccount) => number,
validatorLimiter?: (a: ValidatorAccount) => number,
) {
const stakePool = await getStakePoolAccount(connection, stakePoolAddress);
const poolAmount = solToLamports(amount);
Expand Down Expand Up @@ -391,21 +392,21 @@ export async function withdrawStake(
} else {
// Get the list of accounts to withdraw from
withdrawAccounts.push(
...(await prepareWithdrawAccounts(
...(await prepareWithdrawAccounts({
connection,
stakePool.account.data,
stakePoolAddress,
poolAmount,
validatorComparator,
poolTokenAccount.equals(stakePool.account.data.managerFeeAccount),
)),
stakePool: stakePool.account.data,
amount: poolAmount,
comparator: validatorComparator,
limiter: validatorLimiter,
skipFee: poolTokenAccount.equals(stakePool.account.data.managerFeeAccount),
})),
);
}

// Construct transaction to withdraw from withdrawAccounts account list
const instructions: TransactionInstruction[] = [];
const userTransferAuthority = Keypair.generate();

const signers: Signer[] = [userTransferAuthority];

instructions.push(
Expand All @@ -421,15 +422,8 @@ export async function withdrawStake(

let totalRentFreeBalances = 0;

// Max 5 accounts to prevent an error: "Transaction too large"
const maxWithdrawAccounts = 5;
let i = 0;

// Go through prepared accounts and withdraw/claim them
for (const withdrawAccount of withdrawAccounts) {
if (i > maxWithdrawAccounts) {
break;
}
// Convert pool tokens amount to lamports
const solWithdrawAmount = Math.ceil(
calcLamportsWithdrawAmount(stakePool.account.data, withdrawAccount.poolAmount),
Expand Down Expand Up @@ -471,7 +465,6 @@ export async function withdrawStake(
withdrawAuthority,
}),
);
i++;
}

return {
Expand Down Expand Up @@ -907,3 +900,80 @@ export async function stakePoolInfo(connection: Connection, stakePoolAddress: Pu
}, // CliStakePoolDetails
};
}

/**
* Creates instructions required to create pool token metadata.
*/
export async function createPoolTokenMetadata(
connection: Connection,
stakePoolAddress: PublicKey,
tokenMetadata: PublicKey,
name: string,
symbol: string,
uri: string,
payer?: PublicKey,
) {
const stakePool = await getStakePoolAccount(connection, stakePoolAddress);

const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
STAKE_POOL_PROGRAM_ID,
stakePoolAddress,
);

const manager = stakePool.account.data.manager;

const instructions: TransactionInstruction[] = [];
instructions.push(
StakePoolInstruction.createTokenMetadata({
stakePool: stakePoolAddress,
poolMint: stakePool.account.data.poolMint,
payer: payer ?? manager,
manager,
tokenMetadata,
withdrawAuthority,
name,
symbol,
uri,
}),
);

return {
instructions,
};
}

/**
* Creates instructions required to update pool token metadata.
*/
export async function updatePoolTokenMetadata(
connection: Connection,
stakePoolAddress: PublicKey,
tokenMetadata: PublicKey,
name: string,
symbol: string,
uri: string,
) {
const stakePool = await getStakePoolAccount(connection, stakePoolAddress);

const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
STAKE_POOL_PROGRAM_ID,
stakePoolAddress,
);

const instructions: TransactionInstruction[] = [];
instructions.push(
StakePoolInstruction.updateTokenMetadata({
stakePool: stakePoolAddress,
manager: stakePool.account.data.manager,
tokenMetadata,
withdrawAuthority,
name,
symbol,
uri,
}),
);

return {
instructions,
};
}
172 changes: 112 additions & 60 deletions stake-pool/js/src/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ import {
} from '@solana/web3.js';
import * as BufferLayout from '@solana/buffer-layout';
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
import { STAKE_POOL_PROGRAM_ID } from './constants';
import { InstructionType, encodeData, decodeData } from './utils';
import {
METADATA_MAX_NAME_LENGTH,
METADATA_MAX_SYMBOL_LENGTH,
METADATA_MAX_URI_LENGTH,
METADATA_PROGRAM_ID,
STAKE_POOL_PROGRAM_ID,
} from './constants';
import { InstructionType, encodeData } from './utils';

/**
* An enumeration of valid StakePoolInstructionType's
Expand All @@ -25,7 +31,9 @@ export type StakePoolInstructionType =
| 'DepositStake'
| 'DepositSol'
| 'WithdrawStake'
| 'WithdrawSol';
| 'WithdrawSol'
| 'UpdateTokenMetadata'
| 'CreateTokenMetadata';

const MOVE_STAKE_LAYOUT = BufferLayout.struct<any>([
BufferLayout.u8('instruction'),
Expand All @@ -39,6 +47,13 @@ const UPDATE_VALIDATOR_LIST_BALANCE_LAYOUT = BufferLayout.struct<any>([
BufferLayout.u8('noMerge'),
]);

const TOKEN_METADATA_LAYOUT = BufferLayout.struct<any>([
BufferLayout.u8('instruction'),
BufferLayout.blob(METADATA_MAX_NAME_LENGTH, 'name'),
BufferLayout.blob(METADATA_MAX_SYMBOL_LENGTH, 'symbol'),
BufferLayout.blob(METADATA_MAX_URI_LENGTH, 'uri'),
Comment on lines +52 to +54
Copy link
Contributor

Choose a reason for hiding this comment

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

This probably won't work -- the stake pool program is expecting a Borsh-encoded String, which means 4 bytes for the length, followed by utf-8 bytes

]);

/**
* An enumeration of valid stake InstructionType's
* @internal
Expand Down Expand Up @@ -96,6 +111,18 @@ export const STAKE_POOL_INSTRUCTION_LAYOUTS: {
BufferLayout.ns64('poolTokens'),
]),
},
/// Create token metadata for the stake-pool token in the
/// metaplex-token program
CreateTokenMetadata: {
index: 17,
layout: TOKEN_METADATA_LAYOUT,
},
/// Update token metadata for the stake-pool token in the
/// metaplex-token program
UpdateTokenMetadata: {
index: 18,
layout: TOKEN_METADATA_LAYOUT,
},
});

/**
Expand Down Expand Up @@ -232,6 +259,28 @@ export type DepositSolParams = {
lamports: number;
};

export type CreateTokenMetadataParams = {
stakePool: PublicKey;
manager: PublicKey;
tokenMetadata: PublicKey;
withdrawAuthority: PublicKey;
poolMint: PublicKey;
payer: PublicKey;
name: string;
symbol: string;
uri: string;
};

export type UpdateTokenMetadataParams = {
stakePool: PublicKey;
manager: PublicKey;
tokenMetadata: PublicKey;
withdrawAuthority: PublicKey;
name: string;
symbol: string;
uri: string;
};

/**
* Stake Pool Instruction class
*/
Expand Down Expand Up @@ -604,69 +653,72 @@ export class StakePoolInstruction {
}

/**
* Decode a deposit stake pool instruction and retrieve the instruction params.
* Creates an instruction to create metadata
* using the mpl token metadata program for the pool token
*/
static decodeDepositStake(instruction: TransactionInstruction): DepositStakeParams {
this.checkProgramId(instruction.programId);
this.checkKeyLength(instruction.keys, 11);

decodeData(STAKE_POOL_INSTRUCTION_LAYOUTS.DepositStake, instruction.data);

return {
stakePool: instruction.keys[0].pubkey,
validatorList: instruction.keys[1].pubkey,
depositAuthority: instruction.keys[2].pubkey,
withdrawAuthority: instruction.keys[3].pubkey,
depositStake: instruction.keys[4].pubkey,
validatorStake: instruction.keys[5].pubkey,
reserveStake: instruction.keys[6].pubkey,
destinationPoolAccount: instruction.keys[7].pubkey,
managerFeeAccount: instruction.keys[8].pubkey,
referralPoolAccount: instruction.keys[9].pubkey,
poolMint: instruction.keys[10].pubkey,
};
}
static createTokenMetadata(params: CreateTokenMetadataParams): TransactionInstruction {
const {
stakePool,
withdrawAuthority,
tokenMetadata,
manager,
payer,
poolMint,
name,
symbol,
uri,
} = params;

/**
* Decode a deposit sol instruction and retrieve the instruction params.
*/
static decodeDepositSol(instruction: TransactionInstruction): DepositSolParams {
this.checkProgramId(instruction.programId);
this.checkKeyLength(instruction.keys, 9);

const { amount } = decodeData(STAKE_POOL_INSTRUCTION_LAYOUTS.DepositSol, instruction.data);

return {
stakePool: instruction.keys[0].pubkey,
depositAuthority: instruction.keys[1].pubkey,
withdrawAuthority: instruction.keys[2].pubkey,
reserveStake: instruction.keys[3].pubkey,
fundingAccount: instruction.keys[4].pubkey,
destinationPoolAccount: instruction.keys[5].pubkey,
managerFeeAccount: instruction.keys[6].pubkey,
referralPoolAccount: instruction.keys[7].pubkey,
poolMint: instruction.keys[8].pubkey,
lamports: amount,
};
}
const keys = [
{ pubkey: stakePool, isSigner: false, isWritable: false },
{ pubkey: manager, isSigner: true, isWritable: false },
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
{ pubkey: poolMint, isSigner: false, isWritable: false },
{ pubkey: payer, isSigner: true, isWritable: true },
{ pubkey: tokenMetadata, isSigner: false, isWritable: true },
{ pubkey: METADATA_PROGRAM_ID, isSigner: false, isWritable: false },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
];

/**
* @internal
*/
private static checkProgramId(programId: PublicKey) {
if (!programId.equals(StakeProgram.programId)) {
throw new Error('Invalid instruction; programId is not StakeProgram');
}
const data = encodeData(STAKE_POOL_INSTRUCTION_LAYOUTS.CreateTokenMetadata, {
name: new TextEncoder().encode(name.padEnd(METADATA_MAX_NAME_LENGTH, '\0')),
symbol: new TextEncoder().encode(symbol.padEnd(METADATA_MAX_SYMBOL_LENGTH, '\0')),
uri: new TextEncoder().encode(uri.padEnd(METADATA_MAX_URI_LENGTH, '\0')),
});

return new TransactionInstruction({
programId: STAKE_POOL_PROGRAM_ID,
keys,
data,
});
}

/**
* @internal
* Creates an instruction to update metadata
* in the mpl token metadata program account for the pool token
*/
private static checkKeyLength(keys: Array<any>, expectedLength: number) {
if (keys.length < expectedLength) {
throw new Error(
`Invalid instruction; found ${keys.length} keys, expected at least ${expectedLength}`,
);
}
static updateTokenMetadata(params: UpdateTokenMetadataParams): TransactionInstruction {
const { stakePool, withdrawAuthority, tokenMetadata, manager, name, symbol, uri } = params;

const keys = [
{ pubkey: stakePool, isSigner: false, isWritable: false },
{ pubkey: manager, isSigner: true, isWritable: false },
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
{ pubkey: tokenMetadata, isSigner: false, isWritable: true },
{ pubkey: METADATA_PROGRAM_ID, isSigner: false, isWritable: false },
];

const data = encodeData(STAKE_POOL_INSTRUCTION_LAYOUTS.UpdateTokenMetadata, {
name: new TextEncoder().encode(name.padEnd(METADATA_MAX_NAME_LENGTH, '\0')),
symbol: new TextEncoder().encode(symbol.padEnd(METADATA_MAX_SYMBOL_LENGTH, '\0')),
uri: new TextEncoder().encode(uri.padEnd(METADATA_MAX_URI_LENGTH, '\0')),
});

return new TransactionInstruction({
programId: STAKE_POOL_PROGRAM_ID,
keys,
data,
});
}
}
Loading