Skip to content
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
19 changes: 13 additions & 6 deletions js/compressed-token/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

### Breaking Changes

- **`transferInterface` and `createTransferInterfaceInstructions`**: `destination` is now the token account address (SPL-style), not the recipient wallet. `ensureRecipientAta` removed; caller must create the destination ATA before transfer via `getOrCreateAtaInterface` or `createAssociatedTokenAccountInterfaceIdempotentInstruction`.
- **Action:** `transferInterface(rpc, payer, source, mint, destination, owner, amount, ...)` — `destination` is the token account (e.g. `getAssociatedTokenAddressInterface(mint, recipient.publicKey)`).
- **Instruction builder:** `createTransferInterfaceInstructions(rpc, payer, mint, amount, sender, destination, decimals, options?)` — same `destination` semantics.
- **`decimals` is now required** on `createTransferInterfaceInstructions` (instruction-level API). Fetch with `getMintInterface(rpc, mint).mint.decimals` if you were not already threading mint decimals.
- **`transferInterface` and `createTransferInterfaceInstructions`** now take a recipient wallet address and ensure recipient ATA internally.
- **Action:** `transferInterface(rpc, payer, source, mint, recipient, owner, amount, ...)` — `recipient` is the wallet public key.
- **Instruction builder:** `createTransferInterfaceInstructions(rpc, payer, mint, amount, sender, recipient, decimals, options?)` — derives destination ATA and inserts idempotent ATA-create internally.
- **Advanced explicit-account path:** use `transferToAccountInterface(...)` and `createTransferToAccountInterfaceInstructions(...)` for destination token-account routing (program-owned/custom accounts), preserving the previous destination-account behavior.
- **`decimals` is required** on v3 action-level instruction builders. Fetch with `getMintInterface(rpc, mint).mint.decimals` if not already threaded.

- **Root export removed:** `createLoadAtaInstructionsFromInterface` is no longer exported from the package root. Use `createLoadAtaInstructions` (public API) and pass ATA/owner/mint directly.

Expand Down Expand Up @@ -80,7 +81,12 @@ const batches = await createTransferInterfaceInstructions(
const { rest: loads, last: transferTx } = sliceLast(batches);
```

Options include `ensureRecipientAta` (default: `true`) which prepends an idempotent ATA creation instruction to the transfer transaction, and `programId` which dispatches to SPL `transferChecked` for `TOKEN_PROGRAM_ID`/`TOKEN_2022_PROGRAM_ID`.
Options at this point included `ensureRecipientAta` (default: `true`) and
`programId`. `ensureRecipientAta` was removed again in `0.23.0-beta.10` when
the split was introduced:
`transferInterface/createTransferInterfaceInstructions` (wallet-recipient) and
`transferToAccountInterface/createTransferToAccountInterfaceInstructions`
(explicit destination-account).

#### `createLoadAtaInstructions`

Expand Down Expand Up @@ -127,7 +133,8 @@ Options include `ensureRecipientAta` (default: `true`) which prepends an idempot

- **`createTransferInterfaceInstructions`**: Instruction builder for transfers with multi-transaction batching, frozen account pre-checks, zero-amount rejection, and `programId`-based dispatch (Light token vs SPL `transferChecked`).
- **`sliceLast`** helper: Splits instruction batches into `{ rest, last }` for parallel-then-sequential sending.
- **`TransferOptions`** interface: `wrap`, `programId`, `ensureRecipientAta`, extends `InterfaceOptions`.
- **`TransferOptions`** at this point included:
`wrap`, `programId`, `ensureRecipientAta`, extends `InterfaceOptions`.
- **Version-aware proof chunking**: V1 inputs chunked with sizes {8,4,2,1}, V2 with {8,7,6,5,4,3,2,1}. V1 and V2 never mixed in a single proof request.
- **`assertUniqueInputHashes`**: Runtime enforcement that no compressed account hash appears in more than one parallel batch.
- **`chunkAccountsByTreeVersion`**: Exported utility for splitting compressed accounts by tree version into prover-compatible groups.
Expand Down
312 changes: 107 additions & 205 deletions js/compressed-token/docs/interface.md

Large diffs are not rendered by default.

51 changes: 32 additions & 19 deletions js/compressed-token/docs/payment-integration.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,51 @@
# Payment Integration: `createTransferInterfaceInstructions`

Build transfer instructions for production payment flows. Returns
`TransactionInstruction[][]` with CU budgeting, sender ATA creation,
loading (decompression), and the transfer instruction. Destination token
account must exist; create it via `getOrCreateAtaInterface` or
`createAssociatedTokenAccountInterfaceIdempotentInstruction` before transfer.
`TransactionInstruction[][]` with load/decompression batches followed by the
final transfer batch.

`createTransferInterfaceInstructions` now takes a **recipient wallet** and
derives/ensures destination ATA internally.

If you need an explicit destination token account (program-owned/custom), use
`createTransferToAccountInterfaceInstructions`.

## Import

```typescript
// Standard (no SPL/T22 wrapping; decimals required)
import {
createTransferInterfaceInstructions,
getAssociatedTokenAddressInterface,
getOrCreateAtaInterface,
createAssociatedTokenAccountInterfaceIdempotentInstruction,
createTransferToAccountInterfaceInstructions,
getMintInterface,
sliceLast,
} from '@lightprotocol/compressed-token';

// Unified (auto-wraps SPL/T22 to c-token ATA; decimals resolved internally)
import {
createTransferInterfaceInstructions,
getAssociatedTokenAddressInterface,
getOrCreateAtaInterface,
createTransferToAccountInterfaceInstructions,
sliceLast,
} from '@lightprotocol/compressed-token/unified';
```

## Usage

```typescript
// 1. Ensure destination exists, then build instruction batches
const destination = getAssociatedTokenAddressInterface(
mint,
recipient.publicKey,
);
await getOrCreateAtaInterface(rpc, payer, mint, recipient.publicKey);

// Standard path: decimals is required (7th arg). Unified path: omit decimals (fetched internally).
// 1. Build instruction batches (wallet-recipient path)
// Standard path: decimals is required. Unified path resolves decimals internally.
const decimals = (await getMintInterface(rpc, mint)).mint.decimals;
const batches = await createTransferInterfaceInstructions(
rpc,
payer.publicKey,
mint,
amount,
sender.publicKey,
destination,
recipient.publicKey,
decimals, // omit when using unified import
);

// 2. Customize (optional) -- append memo, priority fee, etc. to the last batch
// 2. Customize (optional) -- append memo/priority fee to the final batch
batches.at(-1)!.push(memoIx);

// 3. Build all transactions
Expand All @@ -66,6 +61,23 @@ await Promise.all(rest.map(tx => send(tx)));
await send(last);
```

## Explicit destination-account variant

```typescript
const destinationTokenAccount = /* PDA or program-owned token account */;
const decimals = (await getMintInterface(rpc, mint)).mint.decimals;

const batches = await createTransferToAccountInterfaceInstructions(
rpc,
payer.publicKey,
mint,
amount,
sender.publicKey,
destinationTokenAccount,
decimals,
);
```

## Return type

`TransactionInstruction[][]` -- an array of transaction instruction arrays.
Expand All @@ -89,6 +101,7 @@ Use `sliceLast(batches)` to get `{ rest, last }` for clean send orchestration.
| --------------------------- | :-------------------------------------------------------------------------------: | :------------------: |
| `ComputeBudgetProgram` | yes | yes |
| Sender (owner) ATA creation | yes (idempotent) | yes (if needed) |
| Recipient ATA creation | -- | yes |
| Decompress instructions | yes | yes (if needed) |
| Wrap SPL/T22 (unified only) | first load batch (when multiple batches); single batch = load + transfer together | -- |
| Transfer instruction | -- | yes |
Expand Down
2 changes: 2 additions & 0 deletions js/compressed-token/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export {
getAssociatedTokenAddressInterface,
getOrCreateAtaInterface,
transferInterface,
transferToAccountInterface,
createTransferInterfaceInstructions,
createTransferToAccountInterfaceInstructions,
sliceLast,
wrap,
mintTo as mintToLightToken,
Expand Down
138 changes: 133 additions & 5 deletions js/compressed-token/src/v3/actions/transfer-interface.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
ConfirmOptions,
ComputeBudgetProgram,
PublicKey,
Signer,
TransactionInstruction,
TransactionSignature,
} from '@solana/web3.js';
import {
Expand All @@ -13,11 +15,13 @@ import {
LIGHT_TOKEN_PROGRAM_ID,
} from '@lightprotocol/stateless.js';
import BN from 'bn.js';
import { createTransferInterfaceInstructions } from '../instructions/transfer-interface';
import { createTransferToAccountInterfaceInstructions } from '../instructions/transfer-interface';
import { getAssociatedTokenAddressInterface } from '../get-associated-token-address-interface';
import { getMintInterface } from '../get-mint-interface';
import { type SplInterfaceInfo } from '../../utils/get-token-pool-infos';
import { sliceLast } from './slice-last';
import { createAssociatedTokenAccountInterfaceIdempotentInstruction } from '../instructions/create-ata-interface';
import { assertTransactionSizeWithinLimit } from '../utils/estimate-tx-size';

export interface InterfaceOptions {
splInterfaceInfos?: SplInterfaceInfo[];
Expand All @@ -28,7 +32,7 @@ export interface InterfaceOptions {
owner?: PublicKey;
}

export async function transferInterface(
export async function transferToAccountInterface(
rpc: Rpc,
payer: Signer,
source: PublicKey,
Expand Down Expand Up @@ -61,7 +65,7 @@ export async function transferInterface(

const resolvedDecimals =
decimals ?? (await getMintInterface(rpc, mint)).mint.decimals;
const batches = await createTransferInterfaceInstructions(
const batches = await createTransferToAccountInterfaceInstructions(
rpc,
payer.publicKey,
mint,
Expand All @@ -78,15 +82,73 @@ export async function transferInterface(

const additionalSigners = dedupeSigner(payer, [owner]);
const { rest: loads, last: transferIxs } = sliceLast(batches);

await Promise.all(
loads.map(async ixs => {
const { blockhash } = await rpc.getLatestBlockhash();
const tx = buildAndSignTx(ixs, payer, blockhash, additionalSigners);
return sendAndConfirmTx(rpc, tx, confirmOptions);
}),
);
const { blockhash } = await rpc.getLatestBlockhash();
const tx = buildAndSignTx(transferIxs, payer, blockhash, additionalSigners);
return sendAndConfirmTx(rpc, tx, confirmOptions);
}

export async function transferInterface(
rpc: Rpc,
payer: Signer,
source: PublicKey,
mint: PublicKey,
recipient: PublicKey,
owner: Signer,
amount: number | bigint | BN,
programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID,
confirmOptions?: ConfirmOptions,
options?: InterfaceOptions,
wrap = false,
decimals?: number,
): Promise<TransactionSignature> {
assertBetaEnabled();

const effectiveOwner = options?.owner ?? owner.publicKey;
const expectedSource = getAssociatedTokenAddressInterface(
mint,
effectiveOwner,
false,
programId,
);
if (!source.equals(expectedSource)) {
throw new Error(
`Source mismatch. Expected ${expectedSource.toBase58()}, got ${source.toBase58()}`,
);
}

const resolvedDecimals =
decimals ?? (await getMintInterface(rpc, mint)).mint.decimals;
const batches = await createTransferInterfaceInstructions(
rpc,
payer.publicKey,
mint,
amount,
owner.publicKey,
recipient,
resolvedDecimals,
{
...options,
wrap,
programId,
},
);

const additionalSigners = dedupeSigner(payer, [owner]);
const { rest: loads, last: transferIxs } = sliceLast(batches);
await Promise.all(
loads.map(async ixs => {
const { blockhash } = await rpc.getLatestBlockhash();
const tx = buildAndSignTx(ixs, payer, blockhash, additionalSigners);
return sendAndConfirmTx(rpc, tx, confirmOptions);
}),
);
const { blockhash } = await rpc.getLatestBlockhash();
const tx = buildAndSignTx(transferIxs, payer, blockhash, additionalSigners);
return sendAndConfirmTx(rpc, tx, confirmOptions);
Expand All @@ -96,10 +158,76 @@ export interface TransferOptions extends InterfaceOptions {
wrap?: boolean;
programId?: PublicKey;
}
export type TransferToAccountOptions = TransferOptions;

export { sliceLast } from './slice-last';

export async function createTransferInterfaceInstructions(
rpc: Rpc,
payer: PublicKey,
mint: PublicKey,
amount: number | bigint | BN,
sender: PublicKey,
recipient: PublicKey,
decimals: number,
options?: TransferOptions,
): Promise<TransactionInstruction[][]> {
// Convenience path intentionally derives ATA from a wallet recipient.
// PDA/off-curve recipients should use transferToAccountInterface with an
// explicitly derived destination token account.
const programId = options?.programId ?? LIGHT_TOKEN_PROGRAM_ID;
const destination = getAssociatedTokenAddressInterface(
mint,
recipient,
false,
programId,
);
const batches = await createTransferToAccountInterfaceInstructions(
rpc,
payer,
mint,
amount,
sender,
destination,
decimals,
options,
);

const ensureRecipientAtaIx =
createAssociatedTokenAccountInterfaceIdempotentInstruction(
payer,
destination,
recipient,
mint,
programId,
);

const finalBatch = batches[batches.length - 1];
let insertionIdx = 0;
while (
insertionIdx < finalBatch.length &&
finalBatch[insertionIdx].programId.equals(
ComputeBudgetProgram.programId,
)
) {
insertionIdx += 1;
}

const patchedFinalBatch = [
...finalBatch.slice(0, insertionIdx),
ensureRecipientAtaIx,
...finalBatch.slice(insertionIdx),
];
const numSigners = payer.equals(sender) ? 1 : 2;
assertTransactionSizeWithinLimit(
patchedFinalBatch,
numSigners,
'Final transfer batch',
);
return [...batches.slice(0, -1), patchedFinalBatch];
}

export {
createTransferInterfaceInstructions,
createTransferToAccountInterfaceInstructions,
calculateTransferCU,
} from '../instructions/transfer-interface';
8 changes: 6 additions & 2 deletions js/compressed-token/src/v3/instructions/transfer-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ const LIGHT_TOKEN_TRANSFER_DISCRIMINATOR = 3;
const LIGHT_TOKEN_TRANSFER_CHECKED_DISCRIMINATOR = 12;

const TRANSFER_BASE_CU = 10_000;
const TRANSFER_EXTRA_BUFFER_CU = 10_000;

export function calculateTransferCU(
loadBatch: InternalLoadBatch | null,
): number {
return calculateCombinedCU(TRANSFER_BASE_CU, loadBatch);
return calculateCombinedCU(
TRANSFER_BASE_CU + TRANSFER_EXTRA_BUFFER_CU,
loadBatch,
);
}

/**
Expand Down Expand Up @@ -141,7 +145,7 @@ export function createLightTokenTransferCheckedInstruction(
});
}

export async function createTransferInterfaceInstructions(
export async function createTransferToAccountInterfaceInstructions(
rpc: Rpc,
payer: PublicKey,
mint: PublicKey,
Expand Down
Loading
Loading