Skip to content
Draft
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
3 changes: 3 additions & 0 deletions js/compressed-token/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ export {
createWrapInstruction,
createDecompressInterfaceInstruction,
createTransferInterfaceInstruction,
createTransferInterfaceCheckedInstruction,
createCTokenTransferInstruction,
createCTokenTransferCheckedInstruction,
// Types
TokenMetadataInstructionData,
CompressibleConfig,
Expand All @@ -78,6 +80,7 @@ export {
getAssociatedTokenAddressInterface,
getOrCreateAtaInterface,
transferInterface,
transferInterfaceChecked,
decompressInterface,
wrap,
mintTo as mintToCToken,
Expand Down
196 changes: 158 additions & 38 deletions js/compressed-token/src/v3/actions/transfer-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import BN from 'bn.js';
import { getAtaProgramId } from '../ata-utils';
import {
createTransferInterfaceInstruction,
createTransferInterfaceCheckedInstruction,
createCTokenTransferInstruction,
createCTokenTransferCheckedInstruction,
} from '../instructions/transfer-interface';
import { createAssociatedTokenAccountInterfaceIdempotentInstruction } from '../instructions/create-ata-interface';
import { getAssociatedTokenAddressInterface } from '../get-associated-token-address-interface';
Expand Down Expand Up @@ -77,35 +79,25 @@ function calculateComputeUnits(
}

/**
* Transfer tokens using the c-token interface.
*
* Matches SPL Token's transferChecked signature order. Destination must exist.
* Core transfer logic shared by transferInterface and transferInterfaceChecked.
*
* @param rpc RPC connection
* @param payer Fee payer (signer)
* @param source Source c-token ATA address
* @param mint Mint address
* @param destination Destination c-token ATA address (must exist)
* @param owner Source owner (signer)
* @param amount Amount to transfer
* @param programId Token program ID (default: CTOKEN_PROGRAM_ID)
* @param confirmOptions Optional confirm options
* @param options Optional interface options
* @param wrap Include SPL/T22 wrapping (default: false)
* @returns Transaction signature
* When `checkedDecimals` is provided, uses transfer_checked instructions
* (discriminator 12, includes mint account, validates decimals on-chain).
* When undefined, uses basic transfer instructions (discriminator 3).
*/
export async function transferInterface(
async function _transferInterfaceCore(
rpc: Rpc,
payer: Signer,
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
owner: Signer,
amount: number | bigint | BN,
programId: PublicKey = CTOKEN_PROGRAM_ID,
confirmOptions?: ConfirmOptions,
options?: InterfaceOptions,
wrap = false,
checkedDecimals: number | undefined,
programId: PublicKey,
confirmOptions: ConfirmOptions | undefined,
options: InterfaceOptions | undefined,
wrap: boolean,
): Promise<TransactionSignature> {
assertBetaEnabled();

Expand All @@ -129,16 +121,31 @@ export async function transferInterface(
);
}

instructions.push(
createTransferInterfaceInstruction(
source,
destination,
owner.publicKey,
amountBigInt,
[],
programId,
),
);
if (checkedDecimals !== undefined) {
instructions.push(
createTransferInterfaceCheckedInstruction(
source,
mint,
destination,
owner.publicKey,
amountBigInt,
checkedDecimals,
[],
programId,
),
);
} else {
instructions.push(
createTransferInterfaceInstruction(
source,
destination,
owner.publicKey,
amountBigInt,
[],
programId,
),
);
}

const { blockhash } = await rpc.getLatestBlockhash();
const tx = buildAndSignTx(
Expand Down Expand Up @@ -350,14 +357,27 @@ export async function transferInterface(
}

// Transfer (destination must already exist - like SPL Token)
instructions.push(
createCTokenTransferInstruction(
source,
destination,
owner.publicKey,
amountBigInt,
),
);
if (checkedDecimals !== undefined) {
instructions.push(
createCTokenTransferCheckedInstruction(
source,
mint,
destination,
owner.publicKey,
amountBigInt,
checkedDecimals,
),
);
} else {
instructions.push(
createCTokenTransferInstruction(
source,
destination,
owner.publicKey,
amountBigInt,
),
);
}

// Calculate compute units
const computeUnits = calculateComputeUnits(
Expand All @@ -382,3 +402,103 @@ export async function transferInterface(

return sendAndConfirmTx(rpc, tx, confirmOptions);
}

/**
* Transfer tokens using the c-token interface.
*
* Destination must exist.
*
* @param rpc RPC connection
* @param payer Fee payer (signer)
* @param source Source c-token ATA address
* @param mint Mint address
* @param destination Destination c-token ATA address (must exist)
* @param owner Source owner (signer)
* @param amount Amount to transfer
* @param programId Token program ID (default: CTOKEN_PROGRAM_ID)
* @param confirmOptions Optional confirm options
* @param options Optional interface options
* @param wrap Include SPL/T22 wrapping (default: false)
* @returns Transaction signature
*/
export async function transferInterface(
rpc: Rpc,
payer: Signer,
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
owner: Signer,
amount: number | bigint | BN,
programId: PublicKey = CTOKEN_PROGRAM_ID,
confirmOptions?: ConfirmOptions,
options?: InterfaceOptions,
wrap = false,
): Promise<TransactionSignature> {
return _transferInterfaceCore(
rpc,
payer,
source,
mint,
destination,
owner,
amount,
undefined,
programId,
confirmOptions,
options,
wrap,
);
}

/**
* Transfer tokens using the c-token interface with decimals validation.
*
* Like SPL Token's transferChecked, the on-chain program validates that the
* provided `decimals` matches the mint's decimals field, preventing
* decimal-related transfer errors (e.g. sending 1e9 when you meant 1e6).
*
* Destination must exist.
*
* @param rpc RPC connection
* @param payer Fee payer (signer)
* @param source Source c-token ATA address
* @param mint Mint address
* @param destination Destination c-token ATA address (must exist)
* @param owner Source owner (signer)
* @param amount Amount to transfer
* @param decimals Expected decimals of the mint (validated on-chain)
* @param programId Token program ID (default: CTOKEN_PROGRAM_ID)
* @param confirmOptions Optional confirm options
* @param options Optional interface options
* @param wrap Include SPL/T22 wrapping (default: false)
* @returns Transaction signature
*/
export async function transferInterfaceChecked(
rpc: Rpc,
payer: Signer,
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
owner: Signer,
amount: number | bigint | BN,
decimals: number,
programId: PublicKey = CTOKEN_PROGRAM_ID,
confirmOptions?: ConfirmOptions,
options?: InterfaceOptions,
wrap = false,
): Promise<TransactionSignature> {
return _transferInterfaceCore(
rpc,
payer,
source,
mint,
destination,
owner,
amount,
decimals,
programId,
confirmOptions,
options,
wrap,
);
}
115 changes: 115 additions & 0 deletions js/compressed-token/src/v3/instructions/transfer-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import {
TOKEN_2022_PROGRAM_ID,
TOKEN_PROGRAM_ID,
createTransferInstruction as createSplTransferInstruction,
createTransferCheckedInstruction as createSplTransferCheckedInstruction,
} from '@solana/spl-token';

/**
* c-token transfer instruction discriminator
*/
const CTOKEN_TRANSFER_DISCRIMINATOR = 3;

/**
* c-token transfer_checked instruction discriminator (SPL-compatible)
*/
const CTOKEN_TRANSFER_CHECKED_DISCRIMINATOR = 12;

/**
* Create a c-token transfer instruction.
*
Expand Down Expand Up @@ -96,3 +102,112 @@ export function createTransferInterfaceInstruction(

throw new Error(`Unsupported program ID: ${programId.toBase58()}`);
}

/**
* Create a c-token transfer_checked instruction.
*
* Account order matches SPL Token's transferChecked:
* [source, mint, destination, authority]
*
* On-chain, the program validates that `decimals` matches the mint's decimals
* field, preventing decimal-related transfer errors.
*
* @param source Source c-token account
* @param mint Mint account (used for decimals validation)
* @param destination Destination c-token account
* @param owner Owner of the source account (signer, also pays for compressible extension top-ups)
* @param amount Amount to transfer
* @param decimals Expected decimals of the mint
* @returns Transaction instruction for c-token transfer_checked
*/
export function createCTokenTransferCheckedInstruction(
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
owner: PublicKey,
amount: number | bigint,
decimals: number,
): TransactionInstruction {
// Instruction data format:
// byte 0: discriminator (12)
// bytes 1-8: amount (u64 LE)
// byte 9: decimals (u8)
const data = Buffer.alloc(10);
data.writeUInt8(CTOKEN_TRANSFER_CHECKED_DISCRIMINATOR, 0);
data.writeBigUInt64LE(BigInt(amount), 1);
data.writeUInt8(decimals, 9);

const keys = [
{ pubkey: source, isSigner: false, isWritable: true },
{ pubkey: mint, isSigner: false, isWritable: false },
{ pubkey: destination, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: true },
];

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

/**
* Construct a transfer_checked instruction for SPL/T22/c-token. Defaults to
* c-token program. On-chain, validates that `decimals` matches the mint.
*
* @param source Source token account
* @param mint Mint account
* @param destination Destination token account
* @param owner Owner of the source account (signer)
* @param amount Amount to transfer
* @param decimals Expected decimals of the mint
* @param multiSigners Multi-signers (SPL/T22 only)
* @param programId Token program ID (default: CTOKEN_PROGRAM_ID)
* @returns instruction for transfer_checked
*/
export function createTransferInterfaceCheckedInstruction(
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
owner: PublicKey,
amount: number | bigint,
decimals: number,
multiSigners: (Signer | PublicKey)[] = [],
programId: PublicKey = CTOKEN_PROGRAM_ID,
): TransactionInstruction {
if (programId.equals(CTOKEN_PROGRAM_ID)) {
if (multiSigners.length > 0) {
throw new Error(
'c-token transfer does not support multi-signers. Use a single owner.',
);
}
return createCTokenTransferCheckedInstruction(
source,
mint,
destination,
owner,
amount,
decimals,
);
}

if (
programId.equals(TOKEN_PROGRAM_ID) ||
programId.equals(TOKEN_2022_PROGRAM_ID)
) {
return createSplTransferCheckedInstruction(
source,
mint,
destination,
owner,
amount,
decimals,
multiSigners.map(pk =>
pk instanceof PublicKey ? pk : pk.publicKey,
),
programId,
);
}

throw new Error(`Unsupported program ID: ${programId.toBase58()}`);
}
Loading