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

Commit 013cc22

Browse files
stake-pool-js: implement bindings for token metadata instructions (#5103)
* stake-pool-js: implement bindings for token metadata instructions - revert changes - add token metadata - remove MINIMUM_RESERVE_LAMPORTS * Support dynamic instruction layouts for metadata * Fix merge error * Fixup tests --------- Co-authored-by: Alexander Ray <[email protected]>
1 parent 678b1e5 commit 013cc22

File tree

5 files changed

+285
-2
lines changed

5 files changed

+285
-2
lines changed

stake-pool/js/src/constants.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { Buffer } from 'buffer';
22
import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
33

4+
// Public key that identifies the metadata program.
5+
export const METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');
6+
export const METADATA_MAX_NAME_LENGTH = 32;
7+
export const METADATA_MAX_SYMBOL_LENGTH = 10;
8+
export const METADATA_MAX_URI_LENGTH = 200;
9+
410
// Public key that identifies the SPL Stake Pool program.
511
export const STAKE_POOL_PROGRAM_ID = new PublicKey('SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy');
612

stake-pool/js/src/index.ts

+78
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
lamportsToSol,
2929
solToLamports,
3030
findEphemeralStakeProgramAddress,
31+
findMetadataAddress,
3132
} from './utils';
3233
import { StakePoolInstruction } from './instructions';
3334
import {
@@ -1109,3 +1110,80 @@ export async function redelegate(props: RedelegateProps) {
11091110
instructions,
11101111
};
11111112
}
1113+
1114+
/**
1115+
* Creates instructions required to create pool token metadata.
1116+
*/
1117+
export async function createPoolTokenMetadata(
1118+
connection: Connection,
1119+
stakePoolAddress: PublicKey,
1120+
payer: PublicKey,
1121+
name: string,
1122+
symbol: string,
1123+
uri: string,
1124+
) {
1125+
const stakePool = await getStakePoolAccount(connection, stakePoolAddress);
1126+
1127+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1128+
STAKE_POOL_PROGRAM_ID,
1129+
stakePoolAddress,
1130+
);
1131+
const tokenMetadata = findMetadataAddress(stakePool.account.data.poolMint);
1132+
const manager = stakePool.account.data.manager;
1133+
1134+
const instructions: TransactionInstruction[] = [];
1135+
instructions.push(
1136+
StakePoolInstruction.createTokenMetadata({
1137+
stakePool: stakePoolAddress,
1138+
poolMint: stakePool.account.data.poolMint,
1139+
payer,
1140+
manager,
1141+
tokenMetadata,
1142+
withdrawAuthority,
1143+
name,
1144+
symbol,
1145+
uri,
1146+
}),
1147+
);
1148+
1149+
return {
1150+
instructions,
1151+
};
1152+
}
1153+
1154+
/**
1155+
* Creates instructions required to update pool token metadata.
1156+
*/
1157+
export async function updatePoolTokenMetadata(
1158+
connection: Connection,
1159+
stakePoolAddress: PublicKey,
1160+
name: string,
1161+
symbol: string,
1162+
uri: string,
1163+
) {
1164+
const stakePool = await getStakePoolAccount(connection, stakePoolAddress);
1165+
1166+
const withdrawAuthority = await findWithdrawAuthorityProgramAddress(
1167+
STAKE_POOL_PROGRAM_ID,
1168+
stakePoolAddress,
1169+
);
1170+
1171+
const tokenMetadata = findMetadataAddress(stakePool.account.data.poolMint);
1172+
1173+
const instructions: TransactionInstruction[] = [];
1174+
instructions.push(
1175+
StakePoolInstruction.updateTokenMetadata({
1176+
stakePool: stakePoolAddress,
1177+
manager: stakePool.account.data.manager,
1178+
tokenMetadata,
1179+
withdrawAuthority,
1180+
name,
1181+
symbol,
1182+
uri,
1183+
}),
1184+
);
1185+
1186+
return {
1187+
instructions,
1188+
};
1189+
}

stake-pool/js/src/instructions.ts

+141-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ import {
1010
} from '@solana/web3.js';
1111
import * as BufferLayout from '@solana/buffer-layout';
1212
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
13-
import { STAKE_POOL_PROGRAM_ID } from './constants';
1413
import { InstructionType, encodeData, decodeData } from './utils';
1514
import BN from 'bn.js';
15+
import {
16+
METADATA_MAX_NAME_LENGTH,
17+
METADATA_MAX_SYMBOL_LENGTH,
18+
METADATA_MAX_URI_LENGTH,
19+
METADATA_PROGRAM_ID,
20+
STAKE_POOL_PROGRAM_ID,
21+
} from './constants';
1622

1723
/**
1824
* An enumeration of valid StakePoolInstructionType's
@@ -31,6 +37,8 @@ export type StakePoolInstructionType =
3137
| 'DecreaseAdditionalValidatorStake'
3238
| 'Redelegate';
3339

40+
// 'UpdateTokenMetadata' and 'CreateTokenMetadata' have dynamic layouts
41+
3442
const MOVE_STAKE_LAYOUT = BufferLayout.struct<any>([
3543
BufferLayout.u8('instruction'),
3644
BufferLayout.ns64('lamports'),
@@ -43,6 +51,38 @@ const UPDATE_VALIDATOR_LIST_BALANCE_LAYOUT = BufferLayout.struct<any>([
4351
BufferLayout.u8('noMerge'),
4452
]);
4553

54+
export function tokenMetadataLayout(
55+
instruction: number,
56+
nameLength: number,
57+
symbolLength: number,
58+
uriLength: number,
59+
) {
60+
if (nameLength > METADATA_MAX_NAME_LENGTH) {
61+
throw 'maximum token name length is 32 characters';
62+
}
63+
64+
if (symbolLength > METADATA_MAX_SYMBOL_LENGTH) {
65+
throw 'maximum token symbol length is 10 characters';
66+
}
67+
68+
if (uriLength > METADATA_MAX_URI_LENGTH) {
69+
throw 'maximum token uri length is 200 characters';
70+
}
71+
72+
return {
73+
index: instruction,
74+
layout: BufferLayout.struct<any>([
75+
BufferLayout.u8('instruction'),
76+
BufferLayout.u32('nameLen'),
77+
BufferLayout.blob(nameLength, 'name'),
78+
BufferLayout.u32('symbolLen'),
79+
BufferLayout.blob(symbolLength, 'symbol'),
80+
BufferLayout.u32('uriLen'),
81+
BufferLayout.blob(uriLength, 'uri'),
82+
]),
83+
};
84+
}
85+
4686
/**
4787
* An enumeration of valid stake InstructionType's
4888
* @internal
@@ -303,6 +343,28 @@ export type RedelegateParams = {
303343
destinationTransientStakeSeed: number | BN;
304344
};
305345

346+
export type CreateTokenMetadataParams = {
347+
stakePool: PublicKey;
348+
manager: PublicKey;
349+
tokenMetadata: PublicKey;
350+
withdrawAuthority: PublicKey;
351+
poolMint: PublicKey;
352+
payer: PublicKey;
353+
name: string;
354+
symbol: string;
355+
uri: string;
356+
};
357+
358+
export type UpdateTokenMetadataParams = {
359+
stakePool: PublicKey;
360+
manager: PublicKey;
361+
tokenMetadata: PublicKey;
362+
withdrawAuthority: PublicKey;
363+
name: string;
364+
symbol: string;
365+
uri: string;
366+
};
367+
306368
/**
307369
* Stake Pool Instruction class
308370
*/
@@ -823,6 +885,84 @@ export class StakePoolInstruction {
823885
});
824886
}
825887

888+
/**
889+
* Creates an instruction to create metadata
890+
* using the mpl token metadata program for the pool token
891+
*/
892+
static createTokenMetadata(params: CreateTokenMetadataParams): TransactionInstruction {
893+
const {
894+
stakePool,
895+
withdrawAuthority,
896+
tokenMetadata,
897+
manager,
898+
payer,
899+
poolMint,
900+
name,
901+
symbol,
902+
uri,
903+
} = params;
904+
905+
const keys = [
906+
{ pubkey: stakePool, isSigner: false, isWritable: false },
907+
{ pubkey: manager, isSigner: true, isWritable: false },
908+
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
909+
{ pubkey: poolMint, isSigner: false, isWritable: false },
910+
{ pubkey: payer, isSigner: true, isWritable: true },
911+
{ pubkey: tokenMetadata, isSigner: false, isWritable: true },
912+
{ pubkey: METADATA_PROGRAM_ID, isSigner: false, isWritable: false },
913+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
914+
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
915+
];
916+
917+
const type = tokenMetadataLayout(17, name.length, symbol.length, uri.length);
918+
const data = encodeData(type, {
919+
nameLen: name.length,
920+
name: Buffer.from(name),
921+
symbolLen: symbol.length,
922+
symbol: Buffer.from(symbol),
923+
uriLen: uri.length,
924+
uri: Buffer.from(uri),
925+
});
926+
927+
return new TransactionInstruction({
928+
programId: STAKE_POOL_PROGRAM_ID,
929+
keys,
930+
data,
931+
});
932+
}
933+
934+
/**
935+
* Creates an instruction to update metadata
936+
* in the mpl token metadata program account for the pool token
937+
*/
938+
static updateTokenMetadata(params: UpdateTokenMetadataParams): TransactionInstruction {
939+
const { stakePool, withdrawAuthority, tokenMetadata, manager, name, symbol, uri } = params;
940+
941+
const keys = [
942+
{ pubkey: stakePool, isSigner: false, isWritable: false },
943+
{ pubkey: manager, isSigner: true, isWritable: false },
944+
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
945+
{ pubkey: tokenMetadata, isSigner: false, isWritable: true },
946+
{ pubkey: METADATA_PROGRAM_ID, isSigner: false, isWritable: false },
947+
];
948+
949+
const type = tokenMetadataLayout(18, name.length, symbol.length, uri.length);
950+
const data = encodeData(type, {
951+
nameLen: name.length,
952+
name: Buffer.from(name),
953+
symbolLen: symbol.length,
954+
symbol: Buffer.from(symbol),
955+
uriLen: uri.length,
956+
uri: Buffer.from(uri),
957+
});
958+
959+
return new TransactionInstruction({
960+
programId: STAKE_POOL_PROGRAM_ID,
961+
keys,
962+
data,
963+
});
964+
}
965+
826966
/**
827967
* Decode a deposit stake pool instruction and retrieve the instruction params.
828968
*/

stake-pool/js/src/utils/program-address.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { PublicKey } from '@solana/web3.js';
22
import BN from 'bn.js';
33
import { Buffer } from 'buffer';
4-
import { EPHEMERAL_STAKE_SEED_PREFIX, TRANSIENT_STAKE_SEED_PREFIX } from '../constants';
4+
import {
5+
METADATA_PROGRAM_ID,
6+
EPHEMERAL_STAKE_SEED_PREFIX,
7+
TRANSIENT_STAKE_SEED_PREFIX,
8+
} from '../constants';
59

610
/**
711
* Generates the withdraw authority program address for the stake pool
@@ -67,3 +71,14 @@ export async function findEphemeralStakeProgramAddress(
6771
);
6872
return publicKey;
6973
}
74+
75+
/**
76+
* Generates the metadata program address for the stake pool
77+
*/
78+
export function findMetadataAddress(stakePoolMintAddress: PublicKey) {
79+
const [publicKey] = PublicKey.findProgramAddressSync(
80+
[Buffer.from('metadata'), METADATA_PROGRAM_ID.toBuffer(), stakePoolMintAddress.toBuffer()],
81+
METADATA_PROGRAM_ID,
82+
);
83+
return publicKey;
84+
}

stake-pool/js/test/instructions.test.ts

+44
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import {
2626
withdrawStake,
2727
redelegate,
2828
getStakeAccount,
29+
createPoolTokenMetadata,
30+
updatePoolTokenMetadata,
31+
tokenMetadataLayout,
2932
} from '../src';
3033

3134
import { decodeData } from '../src/utils';
@@ -351,4 +354,45 @@ describe('StakePoolProgram', () => {
351354
expect(decodedData.ephemeralStakeSeed).toBe(data.ephemeralStakeSeed);
352355
});
353356
});
357+
describe('createPoolTokenMetadata', () => {
358+
it('should create pool token metadata', async () => {
359+
connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => {
360+
if (pubKey == stakePoolAddress) {
361+
return stakePoolAccount;
362+
}
363+
return null;
364+
});
365+
const name = 'test';
366+
const symbol = 'TEST';
367+
const uri = 'https://example.com';
368+
369+
const payer = new PublicKey(0);
370+
const res = await createPoolTokenMetadata(
371+
connection,
372+
stakePoolAddress,
373+
payer,
374+
name,
375+
symbol,
376+
uri,
377+
);
378+
379+
const type = tokenMetadataLayout(17, name.length, symbol.length, uri.length);
380+
const data = decodeData(type, res.instructions[0].data);
381+
expect(Buffer.from(data.name).toString()).toBe(name);
382+
expect(Buffer.from(data.symbol).toString()).toBe(symbol);
383+
expect(Buffer.from(data.uri).toString()).toBe(uri);
384+
});
385+
386+
it('should update pool token metadata', async () => {
387+
const name = 'test';
388+
const symbol = 'TEST';
389+
const uri = 'https://example.com';
390+
const res = await updatePoolTokenMetadata(connection, stakePoolAddress, name, symbol, uri);
391+
const type = tokenMetadataLayout(18, name.length, symbol.length, uri.length);
392+
const data = decodeData(type, res.instructions[0].data);
393+
expect(Buffer.from(data.name).toString()).toBe(name);
394+
expect(Buffer.from(data.symbol).toString()).toBe(symbol);
395+
expect(Buffer.from(data.uri).toString()).toBe(uri);
396+
});
397+
});
354398
});

0 commit comments

Comments
 (0)