Skip to content
Merged
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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"format": "prettier --write .",
"format:check": "prettier --check .",
"type-check": "pnpm --recursive run type-check",
"clean": "pnpm --recursive run clean"
"clean": "pnpm --recursive run clean",
"precommit": "pnpm run format && pnpm run lint:fix",
"check": "pnpm run format:check && pnpm run lint && pnpm run type-check"
},
"keywords": [
"solana",
Expand Down
260 changes: 260 additions & 0 deletions packages/sdk/src/administration/__tests__/updateAuthority.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import type { Address, TransactionSigner } from 'gill';
import { AuthorityType, TOKEN_2022_PROGRAM_ADDRESS } from 'gill/programs/token';
import { getUpdateAuthorityInstructions } from '../updateAuthority';
import {
createMockSigner,
generateMockAddress,
TEST_AUTHORITY,
} from '../../__tests__/test-utils';

describe('getUpdateAuthorityInstructions', () => {
let mockMint: Address;
let mockCurrentAuthority: TransactionSigner<string>;
let mockNewAuthority: Address;

beforeEach(() => {
mockMint = generateMockAddress() as Address;
mockCurrentAuthority = createMockSigner();
mockNewAuthority = generateMockAddress() as Address;
});

describe('when role is "Metadata"', () => {
it('should return metadata update authority instruction', () => {
const result = getUpdateAuthorityInstructions({
mint: mockMint,
role: 'Metadata',
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});

expect(Array.isArray(result)).toBe(true);
expect(result).toHaveLength(1);

const instruction = result[0];
expect(instruction).toBeDefined();
expect(instruction.programAddress).toBe(TOKEN_2022_PROGRAM_ADDRESS);
expect(instruction.accounts).toBeDefined();
expect(instruction.data).toBeDefined();
});

it('should handle different mint addresses for metadata', () => {
const customMint = TEST_AUTHORITY as Address;

const result = getUpdateAuthorityInstructions({
mint: customMint,
role: 'Metadata',
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});

expect(result).toHaveLength(1);
expect(result[0].programAddress).toBe(TOKEN_2022_PROGRAM_ADDRESS);
});
});

describe('when role is an AuthorityType', () => {
const authorityTypes = [
AuthorityType.MintTokens,
AuthorityType.FreezeAccount,
AuthorityType.AccountOwner,
AuthorityType.CloseAccount,
AuthorityType.TransferFeeConfig,
AuthorityType.WithheldWithdraw,
AuthorityType.CloseMint,
AuthorityType.InterestRate,
AuthorityType.PermanentDelegate,
AuthorityType.ConfidentialTransferMint,
AuthorityType.TransferHookProgramId,
AuthorityType.ConfidentialTransferFeeConfig,
AuthorityType.MetadataPointer,
AuthorityType.GroupPointer,
AuthorityType.GroupMemberPointer,
AuthorityType.ScaledUiAmount,
AuthorityType.Pause,
];

authorityTypes.forEach(authorityType => {
it(`should return set authority instruction for ${AuthorityType[authorityType]}`, () => {
const result = getUpdateAuthorityInstructions({
mint: mockMint,
role: authorityType,
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});

expect(Array.isArray(result)).toBe(true);
expect(result).toHaveLength(1);

const instruction = result[0];
expect(instruction).toBeDefined();
expect(instruction.programAddress).toBe(TOKEN_2022_PROGRAM_ADDRESS);
expect(instruction.accounts).toBeDefined();
expect(instruction.data).toBeDefined();
});
});

it('should return set authority instruction for MintTokens authority', () => {
const result = getUpdateAuthorityInstructions({
mint: mockMint,
role: AuthorityType.MintTokens,
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});

expect(Array.isArray(result)).toBe(true);
expect(result).toHaveLength(1);
expect(result[0].programAddress).toBe(TOKEN_2022_PROGRAM_ADDRESS);
});
});

describe('edge cases and validation', () => {
it('should handle different new authority addresses', () => {
const customNewAuthority = TEST_AUTHORITY as Address;

const result = getUpdateAuthorityInstructions({
mint: mockMint,
role: AuthorityType.MintTokens,
currentAuthority: mockCurrentAuthority,
newAuthority: customNewAuthority,
});

expect(result).toHaveLength(1);
expect(result[0]).toBeDefined();
});

it('should handle different current authority signers', () => {
const customCurrentAuthority = createMockSigner(TEST_AUTHORITY);

const result = getUpdateAuthorityInstructions({
mint: mockMint,
role: AuthorityType.FreezeAccount,
currentAuthority: customCurrentAuthority,
newAuthority: mockNewAuthority,
});

expect(result).toHaveLength(1);
expect(result[0]).toBeDefined();
});

it('should produce different instructions for metadata vs authority types', () => {
const metadataResult = getUpdateAuthorityInstructions({
mint: mockMint,
role: 'Metadata',
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});

const authorityResult = getUpdateAuthorityInstructions({
mint: mockMint,
role: AuthorityType.MintTokens,
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});

expect(metadataResult).toHaveLength(1);
expect(authorityResult).toHaveLength(1);

// Instructions should be different (different data/accounts structure)
expect(metadataResult[0].data).not.toEqual(authorityResult[0].data);
});
});

describe('function signature and types', () => {
it('should accept all required parameters', () => {
expect(() => {
getUpdateAuthorityInstructions({
mint: mockMint,
role: 'Metadata',
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});
}).not.toThrow();
});

it('should accept AuthorityType enum values', () => {
expect(() => {
getUpdateAuthorityInstructions({
mint: mockMint,
role: AuthorityType.AccountOwner,
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});
}).not.toThrow();
});

it('should return array of instructions with correct structure', () => {
const result = getUpdateAuthorityInstructions({
mint: mockMint,
role: AuthorityType.MintTokens,
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});

expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThan(0);

result.forEach(instruction => {
expect(typeof instruction).toBe('object');
expect(instruction).toHaveProperty('programAddress');
expect(instruction).toHaveProperty('accounts');
expect(instruction).toHaveProperty('data');
expect(typeof instruction.programAddress).toBe('string');
expect(Array.isArray(instruction.accounts)).toBe(true);
expect(instruction.data instanceof Uint8Array).toBe(true);
});
});
});

describe('consistent behavior', () => {
it('should always return exactly one instruction', () => {
// Test metadata role
const metadataResult = getUpdateAuthorityInstructions({
mint: mockMint,
role: 'Metadata',
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});
expect(metadataResult).toHaveLength(1);

// Test various authority types
const authorityTypes = [
AuthorityType.MintTokens,
AuthorityType.FreezeAccount,
AuthorityType.AccountOwner,
AuthorityType.CloseAccount,
];

authorityTypes.forEach(authorityType => {
const result = getUpdateAuthorityInstructions({
mint: mockMint,
role: authorityType,
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});
expect(result).toHaveLength(1);
});
});

it('should use TOKEN_2022_PROGRAM_ADDRESS for all instructions', () => {
// Test metadata
const metadataResult = getUpdateAuthorityInstructions({
mint: mockMint,
role: 'Metadata',
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});
expect(metadataResult[0].programAddress).toBe(TOKEN_2022_PROGRAM_ADDRESS);

// Test authority type
const authorityResult = getUpdateAuthorityInstructions({
mint: mockMint,
role: AuthorityType.MintTokens,
currentAuthority: mockCurrentAuthority,
newAuthority: mockNewAuthority,
});
expect(authorityResult[0].programAddress).toBe(
TOKEN_2022_PROGRAM_ADDRESS
);
});
});
});
1 change: 1 addition & 0 deletions packages/sdk/src/administration/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './updateAuthority';
100 changes: 100 additions & 0 deletions packages/sdk/src/administration/updateAuthority.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
AuthorityType,
getSetAuthorityInstruction,
getUpdateTokenMetadataUpdateAuthorityInstruction,
TOKEN_2022_PROGRAM_ADDRESS,
} from 'gill/programs/token';
import type {
Address,
Instruction,
FullTransaction,
TransactionVersion,
TransactionMessageWithFeePayer,
TransactionSigner,
Rpc,
SolanaRpcApi,
} from 'gill';
import { createTransaction } from 'gill';

type AuthorityRole = AuthorityType | 'Metadata';

/**
* Returns the appropriate instruction(s) to update the authority for a given mint and role.
*
* If the role is 'Metadata', this will return an instruction to update the metadata update authority.
* Otherwise, it returns a set authority instruction for the specified authority type.
*
* @param input - The parameters for the authority update.
* @param input.mint - The address of the mint whose authority is being updated.
* @param input.role - The authority role to update ('Metadata' or an AuthorityType).
* @param input.currentAuthority - The current authority signer.
* @param input.newAuthority - The new authority address.
* @returns An array containing the instruction to update the authority.
*/
export const getUpdateAuthorityInstructions = (input: {
mint: Address;
role: AuthorityRole;
currentAuthority: TransactionSigner<string>;
newAuthority: Address;
}): Instruction<string>[] => {
if (input.role === 'Metadata') {
return [
getUpdateTokenMetadataUpdateAuthorityInstruction(
{
metadata: input.mint,
updateAuthority: input.currentAuthority,
newUpdateAuthority: input.newAuthority,
},
{
programAddress: TOKEN_2022_PROGRAM_ADDRESS,
}
),
];
}
return [
getSetAuthorityInstruction({
owned: input.mint,
owner: input.currentAuthority.address,
newAuthority: input.newAuthority,
authorityType: input.role,
}),
];
};

/**
* Creates a transaction to update the authority for a given mint and role.
*
* This function fetches the latest blockhash, builds the appropriate instruction(s)
* using `getUpdateAuthorityInstructions`, and returns a full transaction ready to be signed and sent.
*
* @param input - The parameters for the authority update transaction.
* @param input.rpc - The Solana RPC client.
* @param input.payer - The transaction fee payer.
* @param input.mint - The address of the mint whose authority is being updated.
* @param input.role - The authority role to update ('Metadata' or an AuthorityType).
* @param input.currentAuthority - The current authority signer.
* @param input.newAuthority - The new authority address.
* @returns A promise that resolves to the constructed full transaction.
*/
export const getUpdateAuthorityTransaction = async (input: {
rpc: Rpc<SolanaRpcApi>;
payer: TransactionSigner<string>;
mint: Address;
role: AuthorityRole;
currentAuthority: TransactionSigner<string>;
newAuthority: Address;
}): Promise<
FullTransaction<TransactionVersion, TransactionMessageWithFeePayer>
> => {
const instructions = getUpdateAuthorityInstructions(input);
const { value: latestBlockhash } = await input.rpc
.getLatestBlockhash()
.send();

return createTransaction({
feePayer: input.payer,
version: 'legacy',
latestBlockhash,
instructions,
});
};
Loading
Loading