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

Commit 5c47856

Browse files
authored
token-js: Support non-transferable mint (#3256)
1 parent b461406 commit 5c47856

File tree

7 files changed

+172
-0
lines changed

7 files changed

+172
-0
lines changed

token/js/src/extensions/extensionType.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DEFAULT_ACCOUNT_STATE_SIZE } from './defaultAccountState';
66
import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority';
77
import { IMMUTABLE_OWNER_SIZE } from './immutableOwner';
88
import { TRANSFER_FEE_CONFIG_SIZE, TRANSFER_FEE_AMOUNT_SIZE } from './transferFee';
9+
import { NON_TRANSFERABLE_SIZE } from './nonTransferable';
910

1011
export enum ExtensionType {
1112
Uninitialized,
@@ -17,6 +18,7 @@ export enum ExtensionType {
1718
DefaultAccountState,
1819
ImmutableOwner,
1920
MemoTransfer,
21+
NonTransferable,
2022
}
2123

2224
export const TYPE_SIZE = 2;
@@ -44,6 +46,8 @@ export function getTypeLen(e: ExtensionType): number {
4446
return IMMUTABLE_OWNER_SIZE;
4547
case ExtensionType.MemoTransfer:
4648
return 1;
49+
case ExtensionType.NonTransferable:
50+
return NON_TRANSFERABLE_SIZE;
4751
default:
4852
throw Error(`Unknown extension type: ${e}`);
4953
}
@@ -61,6 +65,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
6165
case ExtensionType.ImmutableOwner:
6266
case ExtensionType.MemoTransfer:
6367
case ExtensionType.MintCloseAuthority:
68+
case ExtensionType.NonTransferable:
6469
case ExtensionType.Uninitialized:
6570
return ExtensionType.Uninitialized;
6671
}

token/js/src/extensions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export * from './defaultAccountState/index';
33
export * from './extensionType';
44
export * from './mintCloseAuthority';
55
export * from './immutableOwner';
6+
export * from './nonTransferable';
67
export * from './transferFee/index';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { struct } from '@solana/buffer-layout';
2+
import { Mint } from '../state/mint';
3+
import { ExtensionType, getExtensionData } from './extensionType';
4+
5+
/** Non-transferable state as stored by the program */
6+
export interface NonTransferable {} // eslint-disable-line
7+
8+
/** Buffer layout for de/serializing an account */
9+
export const NonTransferableLayout = struct<NonTransferable>([]);
10+
11+
export const NON_TRANSFERABLE_SIZE = NonTransferableLayout.span;
12+
13+
export function getNonTransferable(mint: Mint): NonTransferable | null {
14+
const extensionData = getExtensionData(ExtensionType.NonTransferable, mint.tlvData);
15+
if (extensionData !== null) {
16+
return NonTransferableLayout.decode(extensionData);
17+
} else {
18+
return null;
19+
}
20+
}

token/js/src/instructions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export * from './initializeMint2'; // 20
2424
export * from './initializeImmutableOwner'; // 22
2525
export * from './initializeMintCloseAuthority'; // 23
2626
export * from './createNativeMint'; // 29
27+
export * from './initializeNonTransferableMint'; // 32
2728

2829
export * from './decode';
2930

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { struct, u8 } from '@solana/buffer-layout';
2+
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
3+
import { TokenInstruction } from './types';
4+
5+
/** Deserialized instruction for the initiation of an immutable owner account */
6+
export interface InitializeNonTransferableMintInstructionData {
7+
instruction: TokenInstruction.InitializeNonTransferableMint;
8+
}
9+
10+
/** The struct that represents the instruction data as it is read by the program */
11+
export const initializeNonTransferableMintInstructionData = struct<InitializeNonTransferableMintInstructionData>([
12+
u8('instruction'),
13+
]);
14+
15+
/**
16+
* Construct an InitializeNonTransferableMint instruction
17+
*
18+
* @param mint Mint Account to make non-transferable
19+
* @param programId SPL Token program account
20+
*
21+
* @return Instruction to add to a transaction
22+
*/
23+
export function createInitializeNonTransferableMintInstruction(
24+
mint: PublicKey,
25+
programId: PublicKey
26+
): TransactionInstruction {
27+
const keys = [{ pubkey: mint, isSigner: false, isWritable: true }];
28+
29+
const data = Buffer.alloc(initializeNonTransferableMintInstructionData.span);
30+
initializeNonTransferableMintInstructionData.encode(
31+
{
32+
instruction: TokenInstruction.InitializeNonTransferableMint,
33+
},
34+
data
35+
);
36+
37+
return new TransactionInstruction({ keys, programId, data });
38+
}

token/js/src/instructions/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ export enum TokenInstruction {
3232
Reallocate = 29,
3333
MemoTransferExtension = 30,
3434
CreateNativeMint = 31,
35+
InitializeNonTransferableMint = 32,
3536
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
createInitializeMintInstruction,
16+
createInitializeNonTransferableMintInstruction,
17+
createInitializeImmutableOwnerInstruction,
18+
createInitializeAccountInstruction,
19+
mintTo,
20+
getAccountLen,
21+
getMint,
22+
getMintLen,
23+
getNonTransferable,
24+
transfer,
25+
ExtensionType,
26+
} from '../../src';
27+
import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common';
28+
29+
const TEST_TOKEN_DECIMALS = 2;
30+
const EXTENSIONS = [ExtensionType.NonTransferable];
31+
describe('nonTransferable', () => {
32+
let connection: Connection;
33+
let payer: Signer;
34+
let mint: PublicKey;
35+
let mintAuthority: Keypair;
36+
before(async () => {
37+
connection = await getConnection();
38+
payer = await newAccountWithLamports(connection, 1000000000);
39+
mintAuthority = 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+
createInitializeNonTransferableMintInstruction(mint, 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('fails transfer', async () => {
62+
const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID);
63+
const nonTransferable = getNonTransferable(mintInfo);
64+
expect(nonTransferable).to.not.be.null;
65+
66+
const owner = Keypair.generate();
67+
const accountLen = getAccountLen([ExtensionType.ImmutableOwner]);
68+
const lamports = await connection.getMinimumBalanceForRentExemption(accountLen);
69+
70+
const sourceKeypair = Keypair.generate();
71+
const source = sourceKeypair.publicKey;
72+
let transaction = new Transaction().add(
73+
SystemProgram.createAccount({
74+
fromPubkey: payer.publicKey,
75+
newAccountPubkey: source,
76+
space: accountLen,
77+
lamports,
78+
programId: TEST_PROGRAM_ID,
79+
}),
80+
createInitializeImmutableOwnerInstruction(source, TEST_PROGRAM_ID),
81+
createInitializeAccountInstruction(source, mint, owner.publicKey, TEST_PROGRAM_ID)
82+
);
83+
await sendAndConfirmTransaction(connection, transaction, [payer, sourceKeypair], undefined);
84+
85+
const destinationKeypair = Keypair.generate();
86+
const destination = destinationKeypair.publicKey;
87+
transaction = new Transaction().add(
88+
SystemProgram.createAccount({
89+
fromPubkey: payer.publicKey,
90+
newAccountPubkey: destination,
91+
space: accountLen,
92+
lamports,
93+
programId: TEST_PROGRAM_ID,
94+
}),
95+
createInitializeImmutableOwnerInstruction(destination, TEST_PROGRAM_ID),
96+
createInitializeAccountInstruction(destination, mint, owner.publicKey, TEST_PROGRAM_ID)
97+
);
98+
await sendAndConfirmTransaction(connection, transaction, [payer, destinationKeypair], undefined);
99+
100+
const amount = BigInt(1000);
101+
await mintTo(connection, payer, mint, source, mintAuthority, amount, [], undefined, TEST_PROGRAM_ID);
102+
103+
expect(transfer(connection, payer, source, destination, owner, amount, [], undefined, TEST_PROGRAM_ID)).to.be
104+
.rejected;
105+
});
106+
});

0 commit comments

Comments
 (0)