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

Commit 2e1286b

Browse files
authored
token-js: Support MintCloseAuthority (#2951)
1 parent af90fac commit 2e1286b

File tree

8 files changed

+274
-2
lines changed

8 files changed

+274
-2
lines changed

token/js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"test": "yarn test:unit && yarn test:e2e-built && yarn test:e2e-native && yarn test:e2e-2022",
3535
"test:unit": "mocha test/unit",
3636
"test:e2e-built": "start-server-and-test 'solana-test-validator --bpf-program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA ../../target/deploy/spl_token.so --reset --quiet' http://localhost:8899/health 'mocha test/e2e'",
37-
"test:e2e-2022": "TEST_PROGRAM_ID=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb start-server-and-test 'solana-test-validator --bpf-program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL ../../target/deploy/spl_associated_token_account.so --bpf-program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb ../../target/deploy/spl_token_2022.so --reset --quiet' http://localhost:8899/health 'mocha test/e2e'",
37+
"test:e2e-2022": "TEST_PROGRAM_ID=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb start-server-and-test 'solana-test-validator --bpf-program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL ../../target/deploy/spl_associated_token_account.so --bpf-program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb ../../target/deploy/spl_token_2022.so --reset --quiet' http://localhost:8899/health 'mocha test/e2e*'",
3838
"test:e2e-native": "start-server-and-test 'solana-test-validator --reset --quiet' http://localhost:8899/health 'mocha test/e2e'",
3939
"docs": "shx rm -rf docs && NODE_OPTIONS=--max_old_space_size=4096 typedoc && shx cp .nojekyll docs/",
4040
"fmt": "prettier --write '{*,**/*}.{js,ts,jsx,tsx,json}'",

token/js/src/extensions/extensionType.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ACCOUNT_SIZE } from '../state/account';
22
import { Mint, MINT_SIZE } from '../state/mint';
33
import { MULTISIG_SIZE } from '../state/multisig';
44
import { ACCOUNT_TYPE_SIZE } from './accountType';
5+
import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority';
56

67
export enum ExtensionType {
78
Uninitialized,
@@ -29,7 +30,7 @@ export function getTypeLen(e: ExtensionType): number {
2930
case ExtensionType.TransferFeeAmount:
3031
return 8;
3132
case ExtensionType.MintCloseAuthority:
32-
return 32;
33+
return MINT_CLOSE_AUTHORITY_SIZE;
3334
case ExtensionType.ConfidentialTransferMint:
3435
return 97;
3536
case ExtensionType.ConfidentialTransferAccount:

token/js/src/extensions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './accountType';
22
export * from './extensionType';
3+
export * from './mintCloseAuthority';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { struct } from '@solana/buffer-layout';
2+
import { publicKey } from '@solana/buffer-layout-utils';
3+
import { PublicKey } from '@solana/web3.js';
4+
import { Mint } from '../state/mint';
5+
import { ExtensionType, getExtensionData } from './extensionType';
6+
7+
/** MintCloseAuthority as stored by the program */
8+
export interface MintCloseAuthority {
9+
closeAuthority: PublicKey;
10+
}
11+
12+
/** Buffer layout for de/serializing a mint */
13+
export const MintCloseAuthorityLayout = struct<MintCloseAuthority>([publicKey('closeAuthority')]);
14+
15+
export const MINT_CLOSE_AUTHORITY_SIZE = MintCloseAuthorityLayout.span;
16+
17+
export function getMintCloseAuthority(mint: Mint): MintCloseAuthority | null {
18+
const extensionData = getExtensionData(ExtensionType.MintCloseAuthority, mint.tlvData);
19+
if (extensionData !== null) {
20+
return MintCloseAuthorityLayout.decode(extensionData);
21+
} else {
22+
return null;
23+
}
24+
}

token/js/src/instructions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './syncNative'; // 17
2121
export * from './initializeAccount3'; // 18
2222
export * from './initializeMultisig2'; // 19
2323
export * from './initializeMint2'; // 20
24+
export * from './initializeMintCloseAuthority'; // 23
2425
export * from './createNativeMint'; // 29
2526

2627
export * from './decode';
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { struct, u8 } from '@solana/buffer-layout';
2+
import { publicKey } from '@solana/buffer-layout-utils';
3+
import { AccountMeta, PublicKey, TransactionInstruction } from '@solana/web3.js';
4+
import {
5+
TokenInvalidInstructionDataError,
6+
TokenInvalidInstructionKeysError,
7+
TokenInvalidInstructionProgramError,
8+
TokenInvalidInstructionTypeError,
9+
} from '../errors';
10+
import { TokenInstruction } from './types';
11+
12+
/** TODO: docs */
13+
export interface InitializeMintCloseAuthorityInstructionData {
14+
instruction: TokenInstruction.InitializeMintCloseAuthority;
15+
closeAuthorityOption: 1 | 0;
16+
closeAuthority: PublicKey;
17+
}
18+
19+
/** TODO: docs */
20+
export const initializeMintCloseAuthorityInstructionData = struct<InitializeMintCloseAuthorityInstructionData>([
21+
u8('instruction'),
22+
u8('closeAuthorityOption'),
23+
publicKey('closeAuthority'),
24+
]);
25+
26+
/**
27+
* Construct an InitializeMintCloseAuthority instruction
28+
*
29+
* @param mint Token mint account
30+
* @param closeAuthority Optional authority that can close the mint
31+
* @param programId SPL Token program account
32+
*
33+
* @return Instruction to add to a transaction
34+
*/
35+
export function createInitializeMintCloseAuthorityInstruction(
36+
mint: PublicKey,
37+
closeAuthority: PublicKey | null,
38+
programId: PublicKey
39+
): TransactionInstruction {
40+
const keys = [{ pubkey: mint, isSigner: false, isWritable: true }];
41+
42+
const data = Buffer.alloc(initializeMintCloseAuthorityInstructionData.span);
43+
initializeMintCloseAuthorityInstructionData.encode(
44+
{
45+
instruction: TokenInstruction.InitializeMintCloseAuthority,
46+
closeAuthorityOption: closeAuthority ? 1 : 0,
47+
closeAuthority: closeAuthority || new PublicKey(0),
48+
},
49+
data
50+
);
51+
52+
return new TransactionInstruction({ keys, programId, data });
53+
}
54+
55+
/** A decoded, valid InitializeMintCloseAuthority instruction */
56+
export interface DecodedInitializeMintCloseAuthorityInstruction {
57+
programId: PublicKey;
58+
keys: {
59+
mint: AccountMeta;
60+
};
61+
data: {
62+
instruction: TokenInstruction.InitializeMintCloseAuthority;
63+
closeAuthority: PublicKey | null;
64+
};
65+
}
66+
67+
/**
68+
* Decode an InitializeMintCloseAuthority instruction and validate it
69+
*
70+
* @param instruction Transaction instruction to decode
71+
* @param programId SPL Token program account
72+
*
73+
* @return Decoded, valid instruction
74+
*/
75+
export function decodeInitializeMintCloseAuthorityInstruction(
76+
instruction: TransactionInstruction,
77+
programId: PublicKey
78+
): DecodedInitializeMintCloseAuthorityInstruction {
79+
if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError();
80+
if (instruction.data.length !== initializeMintCloseAuthorityInstructionData.span)
81+
throw new TokenInvalidInstructionDataError();
82+
83+
const {
84+
keys: { mint },
85+
data,
86+
} = decodeInitializeMintCloseAuthorityInstructionUnchecked(instruction);
87+
if (data.instruction !== TokenInstruction.InitializeMintCloseAuthority)
88+
throw new TokenInvalidInstructionTypeError();
89+
if (!mint) throw new TokenInvalidInstructionKeysError();
90+
91+
return {
92+
programId,
93+
keys: {
94+
mint,
95+
},
96+
data,
97+
};
98+
}
99+
100+
/** A decoded, non-validated InitializeMintCloseAuthority instruction */
101+
export interface DecodedInitializeMintCloseAuthorityInstructionUnchecked {
102+
programId: PublicKey;
103+
keys: {
104+
mint: AccountMeta | undefined;
105+
};
106+
data: {
107+
instruction: number;
108+
closeAuthority: PublicKey | null;
109+
};
110+
}
111+
112+
/**
113+
* Decode an InitializeMintCloseAuthority instruction without validating it
114+
*
115+
* @param instruction Transaction instruction to decode
116+
*
117+
* @return Decoded, non-validated instruction
118+
*/
119+
export function decodeInitializeMintCloseAuthorityInstructionUnchecked({
120+
programId,
121+
keys: [mint],
122+
data,
123+
}: TransactionInstruction): DecodedInitializeMintCloseAuthorityInstructionUnchecked {
124+
const { instruction, closeAuthorityOption, closeAuthority } =
125+
initializeMintCloseAuthorityInstructionData.decode(data);
126+
127+
return {
128+
programId,
129+
keys: {
130+
mint,
131+
},
132+
data: {
133+
instruction,
134+
closeAuthority: closeAuthorityOption ? closeAuthority : null,
135+
},
136+
};
137+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import chai, { expect } from 'chai';
2+
import chaiAsPromised from 'chai-as-promised';
3+
chai.use(chaiAsPromised);
4+
5+
import {
6+
sendAndConfirmTransaction,
7+
Connection,
8+
Keypair,
9+
PublicKey,
10+
Signer,
11+
SystemProgram,
12+
Transaction,
13+
} from '@solana/web3.js';
14+
import {
15+
createAccount,
16+
createInitializeMintInstruction,
17+
createInitializeMintCloseAuthorityInstruction,
18+
closeAccount,
19+
mintTo,
20+
getMintLen,
21+
ExtensionType,
22+
} from '../../src';
23+
import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common';
24+
25+
const TEST_TOKEN_DECIMALS = 2;
26+
const EXTENSIONS = [ExtensionType.MintCloseAuthority];
27+
describe('closeMint', () => {
28+
let connection: Connection;
29+
let payer: Signer;
30+
let mint: PublicKey;
31+
let mintAuthority: Keypair;
32+
let closeAuthority: Keypair;
33+
let account: PublicKey;
34+
let destination: PublicKey;
35+
before(async () => {
36+
connection = await getConnection();
37+
payer = await newAccountWithLamports(connection, 1000000000);
38+
mintAuthority = Keypair.generate();
39+
closeAuthority = Keypair.generate();
40+
});
41+
beforeEach(async () => {
42+
const mintKeypair = Keypair.generate();
43+
mint = mintKeypair.publicKey;
44+
const mintLen = getMintLen(EXTENSIONS);
45+
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
46+
47+
const transaction = new Transaction().add(
48+
SystemProgram.createAccount({
49+
fromPubkey: payer.publicKey,
50+
newAccountPubkey: mint,
51+
space: mintLen,
52+
lamports,
53+
programId: TEST_PROGRAM_ID,
54+
}),
55+
createInitializeMintCloseAuthorityInstruction(mint, closeAuthority.publicKey, TEST_PROGRAM_ID),
56+
createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, mintAuthority.publicKey, null, TEST_PROGRAM_ID)
57+
);
58+
59+
await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair], undefined);
60+
});
61+
it('failsWithNonZeroAmount', async () => {
62+
const owner = Keypair.generate();
63+
destination = Keypair.generate().publicKey;
64+
account = await createAccount(connection, payer, mint, owner.publicKey, undefined, undefined, TEST_PROGRAM_ID);
65+
const amount = BigInt(1000);
66+
await mintTo(connection, payer, mint, account, mintAuthority, amount, [], undefined, TEST_PROGRAM_ID);
67+
expect(closeAccount(connection, payer, mint, destination, closeAuthority, [], undefined, TEST_PROGRAM_ID)).to.be
68+
.rejected;
69+
});
70+
it('works', async () => {
71+
destination = Keypair.generate().publicKey;
72+
const accountInfo = await connection.getAccountInfo(mint);
73+
let rentExemptAmount;
74+
expect(accountInfo).to.not.be.null;
75+
if (accountInfo !== null) {
76+
rentExemptAmount = accountInfo.lamports;
77+
}
78+
79+
await closeAccount(connection, payer, mint, destination, closeAuthority, [], undefined, TEST_PROGRAM_ID);
80+
81+
const closedInfo = await connection.getAccountInfo(mint);
82+
expect(closedInfo).to.be.null;
83+
84+
const destinationInfo = await connection.getAccountInfo(destination);
85+
expect(destinationInfo).to.not.be.null;
86+
if (destinationInfo !== null) {
87+
expect(destinationInfo.lamports).to.eql(rentExemptAmount);
88+
}
89+
});
90+
});

token/js/test/unit/decode.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Keypair } from '@solana/web3.js';
2+
import chai, { expect } from 'chai';
3+
import chaiAsPromised from 'chai-as-promised';
4+
import { createInitializeMintCloseAuthorityInstruction, TOKEN_2022_PROGRAM_ID } from '../../src';
5+
6+
chai.use(chaiAsPromised);
7+
8+
describe('spl-token-2022 instructions', () => {
9+
it('InitializeMintCloseAuthority', () => {
10+
const ix = createInitializeMintCloseAuthorityInstruction(
11+
Keypair.generate().publicKey,
12+
Keypair.generate().publicKey,
13+
TOKEN_2022_PROGRAM_ID
14+
);
15+
expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID);
16+
expect(ix.keys).to.have.length(1);
17+
});
18+
});

0 commit comments

Comments
 (0)