Skip to content

Commit

Permalink
add pausable extension to js-legacy client
Browse files Browse the repository at this point in the history
  • Loading branch information
samkim-crypto committed Jan 13, 2025
1 parent 7644591 commit f2685ec
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 0 deletions.
16 changes: 16 additions & 0 deletions clients/js-legacy/src/extensions/extensionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { MEMO_TRANSFER_SIZE } from './memoTransfer/index.js';
import { METADATA_POINTER_SIZE } from './metadataPointer/state.js';
import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority.js';
import { NON_TRANSFERABLE_SIZE, NON_TRANSFERABLE_ACCOUNT_SIZE } from './nonTransferable.js';
import { PAUSABLE_CONFIG_SIZE, PAUSABLE_ACCOUNT_SIZE } from './pausable/index.js';
import { PERMANENT_DELEGATE_SIZE } from './permanentDelegate.js';
import { TRANSFER_FEE_AMOUNT_SIZE, TRANSFER_FEE_CONFIG_SIZE } from './transferFee/index.js';
import { TRANSFER_HOOK_ACCOUNT_SIZE, TRANSFER_HOOK_SIZE } from './transferHook/index.js';
Expand Down Expand Up @@ -47,6 +48,10 @@ export enum ExtensionType {
TokenGroup = 21,
GroupMemberPointer = 22,
TokenGroupMember = 23,
// ConfidentialMintBurn
// ScaledUi,
PausableConfig = 26,
PausableAccount = 27,
}

export const TYPE_SIZE = 2;
Expand Down Expand Up @@ -111,6 +116,10 @@ export function getTypeLen(e: ExtensionType): number {
return TOKEN_GROUP_SIZE;
case ExtensionType.TokenGroupMember:
return TOKEN_GROUP_MEMBER_SIZE;
case ExtensionType.PausableConfig:
return PAUSABLE_CONFIG_SIZE;
case ExtensionType.PausableAccount:
return PAUSABLE_ACCOUNT_SIZE;
case ExtensionType.TokenMetadata:
throw Error(`Cannot get type length for variable extension type: ${e}`);
default:
Expand All @@ -134,6 +143,7 @@ export function isMintExtension(e: ExtensionType): boolean {
case ExtensionType.GroupMemberPointer:
case ExtensionType.TokenGroup:
case ExtensionType.TokenGroupMember:
case ExtensionType.PausableConfig:
return true;
case ExtensionType.Uninitialized:
case ExtensionType.TransferFeeAmount:
Expand All @@ -143,6 +153,7 @@ export function isMintExtension(e: ExtensionType): boolean {
case ExtensionType.CpiGuard:
case ExtensionType.NonTransferableAccount:
case ExtensionType.TransferHookAccount:
case ExtensionType.PausableAccount:
return false;
default:
throw Error(`Unknown extension type: ${e}`);
Expand All @@ -158,6 +169,7 @@ export function isAccountExtension(e: ExtensionType): boolean {
case ExtensionType.CpiGuard:
case ExtensionType.NonTransferableAccount:
case ExtensionType.TransferHookAccount:
case ExtensionType.PausableAccount:
return true;
case ExtensionType.Uninitialized:
case ExtensionType.TransferFeeConfig:
Expand All @@ -174,6 +186,7 @@ export function isAccountExtension(e: ExtensionType): boolean {
case ExtensionType.GroupMemberPointer:
case ExtensionType.TokenGroup:
case ExtensionType.TokenGroupMember:
case ExtensionType.PausableConfig:
return false;
default:
throw Error(`Unknown extension type: ${e}`);
Expand All @@ -190,6 +203,8 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
return ExtensionType.NonTransferableAccount;
case ExtensionType.TransferHook:
return ExtensionType.TransferHookAccount;
case ExtensionType.PausableAccount:
return ExtensionType.PausableConfig;
case ExtensionType.TransferFeeAmount:
case ExtensionType.ConfidentialTransferAccount:
case ExtensionType.CpiGuard:
Expand All @@ -208,6 +223,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
case ExtensionType.GroupMemberPointer:
case ExtensionType.TokenGroup:
case ExtensionType.TokenGroupMember:
case ExtensionType.PausableConfig:
return ExtensionType.Uninitialized;
}
}
Expand Down
1 change: 1 addition & 0 deletions clients/js-legacy/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from './nonTransferable.js';
export * from './transferFee/index.js';
export * from './permanentDelegate.js';
export * from './transferHook/index.js';
export * from './pausable/index.js';
63 changes: 63 additions & 0 deletions clients/js-legacy/src/extensions/pausable/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js';
import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js';
import { getSigners } from '../../actions/internal.js';
import { TOKEN_2022_PROGRAM_ID } from '../../constants.js';
import { createPauseInstruction, createResumeInstruction } from './instructions.js';

/**
* Pause a pausable mint
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mint Public key of the mint
* @param owner The pausable config authority
* @param multiSigners Signing accounts if `owner` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Public key of the mint
*/
export async function pause(
connection: Connection,
payer: Signer,
mint: PublicKey,
owner: Signer | PublicKey,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_2022_PROGRAM_ID,
): Promise<TransactionSignature> {
const [ownerPublicKey, signers] = getSigners(owner, multiSigners);

const transaction = new Transaction().add(createPauseInstruction(mint, ownerPublicKey, multiSigners, programId));

return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
}

/**
* Resume a pausable mint
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mint Public key of the mint
* @param owner The pausable config authority
* @param multiSigners Signing accounts if `owner` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Public key of the mint
*/
export async function resume(
connection: Connection,
payer: Signer,
mint: PublicKey,
owner: Signer | PublicKey,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_2022_PROGRAM_ID,
): Promise<TransactionSignature> {
const [ownerPublicKey, signers] = getSigners(owner, multiSigners);

const transaction = new Transaction().add(createResumeInstruction(mint, ownerPublicKey, multiSigners, programId));

return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
}
3 changes: 3 additions & 0 deletions clients/js-legacy/src/extensions/pausable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './actions.js';
export * from './instructions.js';
export * from './state.js';
132 changes: 132 additions & 0 deletions clients/js-legacy/src/extensions/pausable/instructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { struct, u8 } from '@solana/buffer-layout';
import { publicKey } from '@solana/buffer-layout-utils';
import type { Signer } from '@solana/web3.js';
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { TOKEN_2022_PROGRAM_ID, programSupportsExtensions } from '../../constants.js';
import { TokenUnsupportedInstructionError } from '../../errors.js';
import { TokenInstruction } from '../../instructions/types.js';
import { addSigners } from '../../instructions/internal.js';

export enum PausableInstruction {
Initialize = 0,
Pause = 1,
Resume = 2,
}

export interface InitializePausableConfigInstructionData {
instruction: TokenInstruction.PausableExtension;
pausableInstruction: PausableInstruction.Initialize;
authority: PublicKey;
}

export const initializePausableConfigInstructionData = struct<InitializePausableConfigInstructionData>([
u8('instruction'),
u8('pausableInstruction'),
publicKey('authority'),
]);

/**
* Construct a InitializePausableConfig instruction
*
* @param mint Token mint account
* @param authority Optional authority that can pause or resume mint
* @param programId SPL Token program account
*/
export function createInitializePausableConfigInstruction(
mint: PublicKey,
authority: PublicKey | null,
programId: PublicKey = TOKEN_2022_PROGRAM_ID,
): TransactionInstruction {
if (!programSupportsExtensions(programId)) {
throw new TokenUnsupportedInstructionError();
}
const keys = [{ pubkey: mint, isSigner: false, isWritable: true }];

const data = Buffer.alloc(initializePausableConfigInstructionData.span);
initializePausableConfigInstructionData.encode(
{
instruction: TokenInstruction.PausableExtension,
pausableInstruction: PausableInstruction.Initialize,
authority: authority ?? PublicKey.default,
},
data,
);

return new TransactionInstruction({ keys, programId, data: data });
}

export interface PauseInstructionData {
instruction: TokenInstruction.PausableExtension;
pausableInstruction: PausableInstruction.Pause;
}

export const pauseInstructionData = struct<PauseInstructionData>([u8('instruction'), u8('pausableInstruction')]);

/**
* Construct a Pause instruction
*
* @param mint Token mint account
* @param authority The pausable mint's authority
* @param multiSigners Signing accounts if authority is a multisig
* @param programId SPL Token program account
*/
export function createPauseInstruction(
mint: PublicKey,
authority: PublicKey,
multiSigners: (Signer | PublicKey)[] = [],
programId: PublicKey = TOKEN_2022_PROGRAM_ID,
): TransactionInstruction {
if (!programSupportsExtensions(programId)) {
throw new TokenUnsupportedInstructionError();
}
const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], authority, multiSigners);

const data = Buffer.alloc(pauseInstructionData.span);
pauseInstructionData.encode(
{
instruction: TokenInstruction.PausableExtension,
pausableInstruction: PausableInstruction.Pause,
},
data,
);

return new TransactionInstruction({ keys, programId, data: data });
}

export interface ResumeInstructionData {
instruction: TokenInstruction.PausableExtension;
pausableInstruction: PausableInstruction.Resume;
}

export const resumeInstructionData = struct<ResumeInstructionData>([u8('instruction'), u8('pausableInstruction')]);

/**
* Construct a Resume instruction
*
* @param mint Token mint account
* @param authority The pausable mint's authority
* @param multiSigners Signing accounts if authority is a multisig
* @param programId SPL Token program account
*/
export function createResumeInstruction(
mint: PublicKey,
authority: PublicKey,
multiSigners: (Signer | PublicKey)[] = [],
programId: PublicKey = TOKEN_2022_PROGRAM_ID,
): TransactionInstruction {
if (!programSupportsExtensions(programId)) {
throw new TokenUnsupportedInstructionError();
}
const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], authority, multiSigners);

const data = Buffer.alloc(resumeInstructionData.span);
resumeInstructionData.encode(
{
instruction: TokenInstruction.PausableExtension,
pausableInstruction: PausableInstruction.Resume,
},
data,
);

return new TransactionInstruction({ keys, programId, data: data });
}
45 changes: 45 additions & 0 deletions clients/js-legacy/src/extensions/pausable/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { struct } from '@solana/buffer-layout';
import { publicKey, bool } from '@solana/buffer-layout-utils';
import type { PublicKey } from '@solana/web3.js';
import type { Account } from '../../state/account.js';
import type { Mint } from '../../state/mint.js';
import { ExtensionType, getExtensionData } from '../extensionType.js';

/** PausableConfig as stored by the program */
export interface PausableConfig {
/** Authority that can pause or resume activity on the mint */
authority: PublicKey;
/** Whether minting / transferring / burning tokens is paused */
paused: boolean;
}

/** Buffer layout for de/serializing a pausable config */
export const PausableConfigLayout = struct<PausableConfig>([publicKey('authority'), bool('paused')]);

export const PAUSABLE_CONFIG_SIZE = PausableConfigLayout.span;

export function getPausableConfig(mint: Mint): PausableConfig | null {
const extensionData = getExtensionData(ExtensionType.PausableConfig, mint.tlvData);
if (extensionData !== null) {
return PausableConfigLayout.decode(extensionData);
} else {
return null;
}
}

/** Pausable token account state as stored by the program */
export interface PausableAccount {} // eslint-disable-line

/** Buffer layout for de/serializing a pausable account */
export const PausableAccountLayout = struct<PausableAccount>([]); // esline-disable-line

export const PAUSABLE_ACCOUNT_SIZE = PausableAccountLayout.span;

export function getPausableAccount(account: Account): PausableAccount | null {
const extensionData = getExtensionData(ExtensionType.PausableAccount, account.tlvData);
if (extensionData !== null) {
return PausableAccountLayout.decode(extensionData);
} else {
return null;
}
}
2 changes: 2 additions & 0 deletions clients/js-legacy/src/instructions/setAuthority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export enum AuthorityType {
MetadataPointer = 12,
GroupPointer = 13,
GroupMemberPointer = 14,
// ScaledUi = 15,
PausableConfig = 16,
}

/** TODO: docs */
Expand Down
3 changes: 3 additions & 0 deletions clients/js-legacy/src/instructions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,7 @@ export enum TokenInstruction {
MetadataPointerExtension = 39,
GroupPointerExtension = 40,
GroupMemberPointerExtension = 41,
// ConfidentialTransferMintBurn = 42,
// ScaledUiExtension = 43,
PausableExtension = 44,
}
Loading

0 comments on commit f2685ec

Please sign in to comment.