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

Commit c3fda0d

Browse files
authored
token-js: Add support for extension data (#2950)
1 parent 5229a21 commit c3fda0d

21 files changed

+234
-86
lines changed

token/js/src/actions/createAccount.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import {
1010
} from '@solana/web3.js';
1111
import { TOKEN_PROGRAM_ID } from '../constants';
1212
import { createInitializeAccountInstruction } from '../instructions/index';
13-
import { ACCOUNT_SIZE, getMinimumBalanceForRentExemptAccount } from '../state/index';
13+
import { getMint } from '../state/index';
1414
import { createAssociatedTokenAccount } from './createAssociatedTokenAccount';
15+
import { getAccountLenForMint } from '../extensions/extensionType';
1516

1617
/**
1718
* Create and initialize a new token account
@@ -39,13 +40,15 @@ export async function createAccount(
3940
if (!keypair) return await createAssociatedTokenAccount(connection, payer, mint, owner, confirmOptions, programId);
4041

4142
// Otherwise, create the account with the provided keypair and return its public key
42-
const lamports = await getMinimumBalanceForRentExemptAccount(connection);
43+
const mintState = await getMint(connection, mint, confirmOptions?.commitment, programId);
44+
const space = getAccountLenForMint(mintState);
45+
const lamports = await connection.getMinimumBalanceForRentExemption(space);
4346

4447
const transaction = new Transaction().add(
4548
SystemProgram.createAccount({
4649
fromPubkey: payer.publicKey,
4750
newAccountPubkey: keypair.publicKey,
48-
space: ACCOUNT_SIZE,
51+
space,
4952
lamports,
5053
programId,
5154
}),

token/js/src/errors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export class TokenAccountNotFoundError extends TokenError {
1010
name = 'TokenAccountNotFoundError';
1111
}
1212

13+
/** Thrown if a program state account is not a valid Account */
14+
export class TokenInvalidAccountError extends TokenError {
15+
name = 'TokenInvalidAccountError';
16+
}
17+
1318
/** Thrown if a program state account is not owned by the expected token program */
1419
export class TokenInvalidAccountOwnerError extends TokenError {
1520
name = 'TokenInvalidAccountOwnerError';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export enum AccountType {
2+
Uninitialized,
3+
Mint,
4+
Account,
5+
}
6+
export const ACCOUNT_TYPE_SIZE = 1;
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { ACCOUNT_SIZE } from '../state/account';
2+
import { Mint, MINT_SIZE } from '../state/mint';
3+
import { MULTISIG_SIZE } from '../state/multisig';
4+
import { ACCOUNT_TYPE_SIZE } from './accountType';
5+
6+
export enum ExtensionType {
7+
Uninitialized,
8+
TransferFeeConfig,
9+
TransferFeeAmount,
10+
MintCloseAuthority,
11+
ConfidentialTransferMint,
12+
ConfidentialTransferAccount,
13+
DefaultAccountState,
14+
ImmutableOwner,
15+
MemoTransfer,
16+
}
17+
18+
export const TYPE_SIZE = 2;
19+
export const LENGTH_SIZE = 2;
20+
21+
// NOTE: All of these should eventually use their type's Span instead of these
22+
// constants. This is provided for at least creation to work.
23+
export function getTypeLen(e: ExtensionType): number {
24+
switch (e) {
25+
case ExtensionType.Uninitialized:
26+
return 0;
27+
case ExtensionType.TransferFeeConfig:
28+
return 108;
29+
case ExtensionType.TransferFeeAmount:
30+
return 8;
31+
case ExtensionType.MintCloseAuthority:
32+
return 32;
33+
case ExtensionType.ConfidentialTransferMint:
34+
return 97;
35+
case ExtensionType.ConfidentialTransferAccount:
36+
return 286;
37+
case ExtensionType.DefaultAccountState:
38+
return 1;
39+
case ExtensionType.ImmutableOwner:
40+
return 0;
41+
case ExtensionType.MemoTransfer:
42+
return 1;
43+
default:
44+
throw Error(`Unknown extension type: ${e}`);
45+
}
46+
}
47+
48+
export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
49+
switch (e) {
50+
case ExtensionType.TransferFeeConfig:
51+
return ExtensionType.TransferFeeAmount;
52+
case ExtensionType.ConfidentialTransferMint:
53+
return ExtensionType.ConfidentialTransferAccount;
54+
case ExtensionType.TransferFeeAmount:
55+
case ExtensionType.ConfidentialTransferAccount:
56+
case ExtensionType.DefaultAccountState:
57+
case ExtensionType.ImmutableOwner:
58+
case ExtensionType.MemoTransfer:
59+
case ExtensionType.MintCloseAuthority:
60+
case ExtensionType.Uninitialized:
61+
return ExtensionType.Uninitialized;
62+
}
63+
}
64+
65+
function getLen(extensionTypes: ExtensionType[], baseSize: number): number {
66+
if (extensionTypes.length === 0) {
67+
return baseSize;
68+
} else {
69+
const accountLength =
70+
ACCOUNT_SIZE +
71+
ACCOUNT_TYPE_SIZE +
72+
extensionTypes
73+
.filter((element, i) => i === extensionTypes.indexOf(element))
74+
.map((element) => getTypeLen(element) + TYPE_SIZE + LENGTH_SIZE)
75+
.reduce((a, b) => a + b);
76+
if (accountLength === MULTISIG_SIZE) {
77+
return accountLength + TYPE_SIZE;
78+
} else {
79+
return accountLength;
80+
}
81+
}
82+
}
83+
84+
export function getMintLen(extensionTypes: ExtensionType[]): number {
85+
return getLen(extensionTypes, MINT_SIZE);
86+
}
87+
88+
export function getAccountLen(extensionTypes: ExtensionType[]): number {
89+
return getLen(extensionTypes, ACCOUNT_SIZE);
90+
}
91+
92+
export function getExtensionData(extension: ExtensionType, tlvData: Buffer): Buffer | null {
93+
let extensionTypeIndex = 0;
94+
while (extensionTypeIndex < tlvData.length) {
95+
const entryType = tlvData.readUInt16LE(extensionTypeIndex);
96+
const entryLength = tlvData.readUInt16LE(extensionTypeIndex + TYPE_SIZE);
97+
const typeIndex = extensionTypeIndex + TYPE_SIZE + LENGTH_SIZE;
98+
if (entryType == extension) {
99+
return tlvData.slice(typeIndex, typeIndex + entryLength);
100+
}
101+
extensionTypeIndex = typeIndex + entryLength;
102+
}
103+
return null;
104+
}
105+
106+
export function getExtensionTypes(tlvData: Buffer): ExtensionType[] {
107+
const extensionTypes = [];
108+
let extensionTypeIndex = 0;
109+
while (extensionTypeIndex < tlvData.length) {
110+
const entryType = tlvData.readUInt16LE(extensionTypeIndex);
111+
extensionTypes.push(entryType);
112+
const entryLength = tlvData.readUInt16LE(extensionTypeIndex + TYPE_SIZE);
113+
extensionTypeIndex += TYPE_SIZE + LENGTH_SIZE + entryLength;
114+
}
115+
return extensionTypes;
116+
}
117+
118+
export function getAccountLenForMint(mint: Mint): number {
119+
const extensionTypes = getExtensionTypes(mint.tlvData);
120+
const accountExtensions = extensionTypes.map(getAccountTypeOfMintType);
121+
return getAccountLen(accountExtensions);
122+
}

token/js/src/extensions/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './accountType';
2+
export * from './extensionType';

token/js/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './extensions/index';
12
export * from './instructions/index';
23
export * from './state/index';
34
export * from './actions/index';

token/js/src/state/account.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ import { struct, u32, u8 } from '@solana/buffer-layout';
22
import { publicKey, u64 } from '@solana/buffer-layout-utils';
33
import { Commitment, Connection, PublicKey } from '@solana/web3.js';
44
import { TOKEN_PROGRAM_ID } from '../constants';
5-
import { TokenAccountNotFoundError, TokenInvalidAccountOwnerError, TokenInvalidAccountSizeError } from '../errors';
5+
import {
6+
TokenAccountNotFoundError,
7+
TokenInvalidAccountError,
8+
TokenInvalidAccountOwnerError,
9+
TokenInvalidAccountSizeError,
10+
} from '../errors';
11+
import { MULTISIG_SIZE } from './multisig';
12+
import { AccountType, ACCOUNT_TYPE_SIZE } from '../extensions/accountType';
13+
import { ExtensionType, getAccountLen } from '../extensions/extensionType';
614

715
/** Information about a token account */
816
export interface Account {
@@ -31,6 +39,7 @@ export interface Account {
3139
rentExemptReserve: bigint | null;
3240
/** Optional authority to close the account */
3341
closeAuthority: PublicKey | null;
42+
tlvData: Buffer;
3443
}
3544

3645
/** Token account state as stored by the program */
@@ -94,7 +103,13 @@ export async function getAccount(
94103
if (!info.owner.equals(programId)) throw new TokenInvalidAccountOwnerError();
95104
if (info.data.length < ACCOUNT_SIZE) throw new TokenInvalidAccountSizeError();
96105

97-
const rawAccount = AccountLayout.decode(info.data);
106+
const rawAccount = AccountLayout.decode(info.data.slice(0, ACCOUNT_SIZE));
107+
let tlvData = Buffer.alloc(0);
108+
if (info.data.length > ACCOUNT_SIZE) {
109+
if (info.data.length === MULTISIG_SIZE) throw new TokenInvalidAccountSizeError();
110+
if (info.data[ACCOUNT_SIZE] != AccountType.Account) throw new TokenInvalidAccountError();
111+
tlvData = info.data.slice(ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE);
112+
}
98113

99114
return {
100115
address,
@@ -108,10 +123,11 @@ export async function getAccount(
108123
isNative: !!rawAccount.isNativeOption,
109124
rentExemptReserve: rawAccount.isNativeOption ? rawAccount.isNative : null,
110125
closeAuthority: rawAccount.closeAuthorityOption ? rawAccount.closeAuthority : null,
126+
tlvData,
111127
};
112128
}
113129

114-
/** Get the minimum lamport balance for a token account to be rent exempt
130+
/** Get the minimum lamport balance for a base token account to be rent exempt
115131
*
116132
* @param connection Connection to use
117133
* @param commitment Desired level of commitment for querying the state
@@ -122,5 +138,21 @@ export async function getMinimumBalanceForRentExemptAccount(
122138
connection: Connection,
123139
commitment?: Commitment
124140
): Promise<number> {
125-
return await connection.getMinimumBalanceForRentExemption(ACCOUNT_SIZE, commitment);
141+
return await getMinimumBalanceForRentExemptAccountWithExtensions(connection, [], commitment);
142+
}
143+
144+
/** Get the minimum lamport balance for a rent-exempt token account with extensions
145+
*
146+
* @param connection Connection to use
147+
* @param commitment Desired level of commitment for querying the state
148+
*
149+
* @return Amount of lamports required
150+
*/
151+
export async function getMinimumBalanceForRentExemptAccountWithExtensions(
152+
connection: Connection,
153+
extensions: ExtensionType[],
154+
commitment?: Commitment
155+
): Promise<number> {
156+
const accountLen = getAccountLen(extensions);
157+
return await connection.getMinimumBalanceForRentExemption(accountLen, commitment);
126158
}

token/js/src/state/extension.ts

Lines changed: 0 additions & 64 deletions
This file was deleted.

token/js/src/state/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export * from './account';
2-
export * from './extension';
32
export * from './mint';
43
export * from './multisig';

0 commit comments

Comments
 (0)