Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[js-legacy] Add pausable extension #59

Merged
merged 2 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 14 additions & 0 deletions clients/js-legacy/src/extensions/extensionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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 { SCALED_UI_AMOUNT_CONFIG_SIZE } from './scaledUiAmount/index.js';
import { TRANSFER_FEE_AMOUNT_SIZE, TRANSFER_FEE_CONFIG_SIZE } from './transferFee/index.js';
Expand Down Expand Up @@ -51,6 +52,8 @@ export enum ExtensionType {
TokenGroupMember = 23,
// ConfidentialMintBurn, // Not implemented yet
ScaledUiAmountConfig = 25,
PausableConfig = 26,
PausableAccount = 27,
}

export const TYPE_SIZE = 2;
Expand Down Expand Up @@ -117,6 +120,10 @@ export function getTypeLen(e: ExtensionType): number {
return TOKEN_GROUP_MEMBER_SIZE;
case ExtensionType.ScaledUiAmountConfig:
return SCALED_UI_AMOUNT_CONFIG_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 @@ -141,6 +148,7 @@ export function isMintExtension(e: ExtensionType): boolean {
case ExtensionType.TokenGroup:
case ExtensionType.TokenGroupMember:
case ExtensionType.ScaledUiAmountConfig:
case ExtensionType.PausableConfig:
return true;
case ExtensionType.Uninitialized:
case ExtensionType.TransferFeeAmount:
Expand All @@ -150,6 +158,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 @@ -165,6 +174,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 @@ -182,6 +192,7 @@ export function isAccountExtension(e: ExtensionType): boolean {
case ExtensionType.TokenGroup:
case ExtensionType.TokenGroupMember:
case ExtensionType.ScaledUiAmountConfig:
case ExtensionType.PausableConfig:
return false;
default:
throw Error(`Unknown extension type: ${e}`);
Expand All @@ -198,6 +209,8 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
return ExtensionType.NonTransferableAccount;
case ExtensionType.TransferHook:
return ExtensionType.TransferHookAccount;
case ExtensionType.PausableConfig:
return ExtensionType.PausableAccount;
case ExtensionType.TransferFeeAmount:
case ExtensionType.ConfidentialTransferAccount:
case ExtensionType.CpiGuard:
Expand All @@ -217,6 +230,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
case ExtensionType.TokenGroup:
case ExtensionType.TokenGroupMember:
case ExtensionType.ScaledUiAmountConfig:
case ExtensionType.PausableAccount:
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 @@ -17,3 +17,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;
}
}
1 change: 1 addition & 0 deletions clients/js-legacy/src/instructions/setAuthority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export enum AuthorityType {
GroupPointer = 13,
GroupMemberPointer = 14,
ScaledUiAmountConfig = 15,
PausableConfig = 16,
}

/** TODO: docs */
Expand Down
1 change: 1 addition & 0 deletions clients/js-legacy/src/instructions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ export enum TokenInstruction {
GroupMemberPointerExtension = 41,
// ConfidentialMintBurnExtension = 42,
ScaledUiAmountExtension = 43,
PausableExtension = 44,
}
Loading
Loading