Skip to content

Commit b61099f

Browse files
chore: js sdk beta fixes (#2354)
* fixes * use slicelast * upd docs * format * upd cu, transferoptions, * add check * fmt ci
1 parent 7075dbc commit b61099f

14 files changed

Lines changed: 603 additions & 273 deletions

js/compressed-token/CHANGELOG.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
### Breaking Changes
44

5-
- **`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`.
6-
- **Action:** `transferInterface(rpc, payer, source, mint, destination, owner, amount, ...)``destination` is the token account (e.g. `getAssociatedTokenAddressInterface(mint, recipient.publicKey)`).
7-
- **Instruction builder:** `createTransferInterfaceInstructions(rpc, payer, mint, amount, sender, destination, decimals, options?)` — same `destination` semantics.
8-
- **`decimals` is now required** on `createTransferInterfaceInstructions` (instruction-level API). Fetch with `getMintInterface(rpc, mint).mint.decimals` if you were not already threading mint decimals.
5+
- **`transferInterface` and `createTransferInterfaceInstructions`** now take a recipient wallet address and ensure recipient ATA internally.
6+
- **Action:** `transferInterface(rpc, payer, source, mint, recipient, owner, amount, ...)``recipient` is the wallet public key.
7+
- **Instruction builder:** `createTransferInterfaceInstructions(rpc, payer, mint, amount, sender, recipient, decimals, options?)` — derives destination ATA and inserts idempotent ATA-create internally.
8+
- **Advanced explicit-account path:** use `transferToAccountInterface(...)` and `createTransferToAccountInterfaceInstructions(...)` for destination token-account routing (program-owned/custom accounts), preserving the previous destination-account behavior.
9+
- **`decimals` is required** on v3 action-level instruction builders. Fetch with `getMintInterface(rpc, mint).mint.decimals` if not already threaded.
910

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

@@ -80,7 +81,12 @@ const batches = await createTransferInterfaceInstructions(
8081
const { rest: loads, last: transferTx } = sliceLast(batches);
8182
```
8283
83-
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`.
84+
Options at this point included `ensureRecipientAta` (default: `true`) and
85+
`programId`. `ensureRecipientAta` was removed again in `0.23.0-beta.10` when
86+
the split was introduced:
87+
`transferInterface/createTransferInterfaceInstructions` (wallet-recipient) and
88+
`transferToAccountInterface/createTransferToAccountInterfaceInstructions`
89+
(explicit destination-account).
8490
8591
#### `createLoadAtaInstructions`
8692
@@ -127,7 +133,8 @@ Options include `ensureRecipientAta` (default: `true`) which prepends an idempot
127133
128134
- **`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`).
129135
- **`sliceLast`** helper: Splits instruction batches into `{ rest, last }` for parallel-then-sequential sending.
130-
- **`TransferOptions`** interface: `wrap`, `programId`, `ensureRecipientAta`, extends `InterfaceOptions`.
136+
- **`TransferOptions`** at this point included:
137+
`wrap`, `programId`, `ensureRecipientAta`, extends `InterfaceOptions`.
131138
- **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.
132139
- **`assertUniqueInputHashes`**: Runtime enforcement that no compressed account hash appears in more than one parallel batch.
133140
- **`chunkAccountsByTreeVersion`**: Exported utility for splitting compressed accounts by tree version into prover-compatible groups.

js/compressed-token/docs/interface.md

Lines changed: 107 additions & 205 deletions
Large diffs are not rendered by default.

js/compressed-token/docs/payment-integration.md

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,51 @@
11
# Payment Integration: `createTransferInterfaceInstructions`
22

33
Build transfer instructions for production payment flows. Returns
4-
`TransactionInstruction[][]` with CU budgeting, sender ATA creation,
5-
loading (decompression), and the transfer instruction. Destination token
6-
account must exist; create it via `getOrCreateAtaInterface` or
7-
`createAssociatedTokenAccountInterfaceIdempotentInstruction` before transfer.
4+
`TransactionInstruction[][]` with load/decompression batches followed by the
5+
final transfer batch.
6+
7+
`createTransferInterfaceInstructions` now takes a **recipient wallet** and
8+
derives/ensures destination ATA internally.
9+
10+
If you need an explicit destination token account (program-owned/custom), use
11+
`createTransferToAccountInterfaceInstructions`.
812

913
## Import
1014

1115
```typescript
1216
// Standard (no SPL/T22 wrapping; decimals required)
1317
import {
1418
createTransferInterfaceInstructions,
15-
getAssociatedTokenAddressInterface,
16-
getOrCreateAtaInterface,
17-
createAssociatedTokenAccountInterfaceIdempotentInstruction,
19+
createTransferToAccountInterfaceInstructions,
1820
getMintInterface,
1921
sliceLast,
2022
} from '@lightprotocol/compressed-token';
2123

2224
// Unified (auto-wraps SPL/T22 to c-token ATA; decimals resolved internally)
2325
import {
2426
createTransferInterfaceInstructions,
25-
getAssociatedTokenAddressInterface,
26-
getOrCreateAtaInterface,
27+
createTransferToAccountInterfaceInstructions,
2728
sliceLast,
2829
} from '@lightprotocol/compressed-token/unified';
2930
```
3031

3132
## Usage
3233

3334
```typescript
34-
// 1. Ensure destination exists, then build instruction batches
35-
const destination = getAssociatedTokenAddressInterface(
36-
mint,
37-
recipient.publicKey,
38-
);
39-
await getOrCreateAtaInterface(rpc, payer, mint, recipient.publicKey);
40-
41-
// Standard path: decimals is required (7th arg). Unified path: omit decimals (fetched internally).
35+
// 1. Build instruction batches (wallet-recipient path)
36+
// Standard path: decimals is required. Unified path resolves decimals internally.
4237
const decimals = (await getMintInterface(rpc, mint)).mint.decimals;
4338
const batches = await createTransferInterfaceInstructions(
4439
rpc,
4540
payer.publicKey,
4641
mint,
4742
amount,
4843
sender.publicKey,
49-
destination,
44+
recipient.publicKey,
5045
decimals, // omit when using unified import
5146
);
5247

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

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

64+
## Explicit destination-account variant
65+
66+
```typescript
67+
const destinationTokenAccount = /* PDA or program-owned token account */;
68+
const decimals = (await getMintInterface(rpc, mint)).mint.decimals;
69+
70+
const batches = await createTransferToAccountInterfaceInstructions(
71+
rpc,
72+
payer.publicKey,
73+
mint,
74+
amount,
75+
sender.publicKey,
76+
destinationTokenAccount,
77+
decimals,
78+
);
79+
```
80+
6981
## Return type
7082

7183
`TransactionInstruction[][]` -- an array of transaction instruction arrays.
@@ -89,6 +101,7 @@ Use `sliceLast(batches)` to get `{ rest, last }` for clean send orchestration.
89101
| --------------------------- | :-------------------------------------------------------------------------------: | :------------------: |
90102
| `ComputeBudgetProgram` | yes | yes |
91103
| Sender (owner) ATA creation | yes (idempotent) | yes (if needed) |
104+
| Recipient ATA creation | -- | yes |
92105
| Decompress instructions | yes | yes (if needed) |
93106
| Wrap SPL/T22 (unified only) | first load batch (when multiple batches); single batch = load + transfer together | -- |
94107
| Transfer instruction | -- | yes |

js/compressed-token/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ export {
7676
getAssociatedTokenAddressInterface,
7777
getOrCreateAtaInterface,
7878
transferInterface,
79+
transferToAccountInterface,
7980
createTransferInterfaceInstructions,
81+
createTransferToAccountInterfaceInstructions,
8082
sliceLast,
8183
wrap,
8284
mintTo as mintToLightToken,

js/compressed-token/src/v3/actions/transfer-interface.ts

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {
22
ConfirmOptions,
3+
ComputeBudgetProgram,
34
PublicKey,
45
Signer,
6+
TransactionInstruction,
57
TransactionSignature,
68
} from '@solana/web3.js';
79
import {
@@ -13,11 +15,13 @@ import {
1315
LIGHT_TOKEN_PROGRAM_ID,
1416
} from '@lightprotocol/stateless.js';
1517
import BN from 'bn.js';
16-
import { createTransferInterfaceInstructions } from '../instructions/transfer-interface';
18+
import { createTransferToAccountInterfaceInstructions } from '../instructions/transfer-interface';
1719
import { getAssociatedTokenAddressInterface } from '../get-associated-token-address-interface';
1820
import { getMintInterface } from '../get-mint-interface';
1921
import { type SplInterfaceInfo } from '../../utils/get-token-pool-infos';
2022
import { sliceLast } from './slice-last';
23+
import { createAssociatedTokenAccountInterfaceIdempotentInstruction } from '../instructions/create-ata-interface';
24+
import { assertTransactionSizeWithinLimit } from '../utils/estimate-tx-size';
2125

2226
export interface InterfaceOptions {
2327
splInterfaceInfos?: SplInterfaceInfo[];
@@ -28,7 +32,7 @@ export interface InterfaceOptions {
2832
owner?: PublicKey;
2933
}
3034

31-
export async function transferInterface(
35+
export async function transferToAccountInterface(
3236
rpc: Rpc,
3337
payer: Signer,
3438
source: PublicKey,
@@ -61,7 +65,7 @@ export async function transferInterface(
6165

6266
const resolvedDecimals =
6367
decimals ?? (await getMintInterface(rpc, mint)).mint.decimals;
64-
const batches = await createTransferInterfaceInstructions(
68+
const batches = await createTransferToAccountInterfaceInstructions(
6569
rpc,
6670
payer.publicKey,
6771
mint,
@@ -78,15 +82,73 @@ export async function transferInterface(
7882

7983
const additionalSigners = dedupeSigner(payer, [owner]);
8084
const { rest: loads, last: transferIxs } = sliceLast(batches);
81-
8285
await Promise.all(
8386
loads.map(async ixs => {
8487
const { blockhash } = await rpc.getLatestBlockhash();
8588
const tx = buildAndSignTx(ixs, payer, blockhash, additionalSigners);
8689
return sendAndConfirmTx(rpc, tx, confirmOptions);
8790
}),
8891
);
92+
const { blockhash } = await rpc.getLatestBlockhash();
93+
const tx = buildAndSignTx(transferIxs, payer, blockhash, additionalSigners);
94+
return sendAndConfirmTx(rpc, tx, confirmOptions);
95+
}
96+
97+
export async function transferInterface(
98+
rpc: Rpc,
99+
payer: Signer,
100+
source: PublicKey,
101+
mint: PublicKey,
102+
recipient: PublicKey,
103+
owner: Signer,
104+
amount: number | bigint | BN,
105+
programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID,
106+
confirmOptions?: ConfirmOptions,
107+
options?: InterfaceOptions,
108+
wrap = false,
109+
decimals?: number,
110+
): Promise<TransactionSignature> {
111+
assertBetaEnabled();
112+
113+
const effectiveOwner = options?.owner ?? owner.publicKey;
114+
const expectedSource = getAssociatedTokenAddressInterface(
115+
mint,
116+
effectiveOwner,
117+
false,
118+
programId,
119+
);
120+
if (!source.equals(expectedSource)) {
121+
throw new Error(
122+
`Source mismatch. Expected ${expectedSource.toBase58()}, got ${source.toBase58()}`,
123+
);
124+
}
89125

126+
const resolvedDecimals =
127+
decimals ?? (await getMintInterface(rpc, mint)).mint.decimals;
128+
const batches = await createTransferInterfaceInstructions(
129+
rpc,
130+
payer.publicKey,
131+
mint,
132+
amount,
133+
owner.publicKey,
134+
recipient,
135+
resolvedDecimals,
136+
{
137+
...options,
138+
wrap,
139+
programId,
140+
},
141+
);
142+
143+
const additionalSigners = dedupeSigner(payer, [owner]);
144+
const { rest: loads, last: transferIxs } = sliceLast(batches);
145+
await Promise.all(
146+
loads.map(async ixs => {
147+
const { blockhash } = await rpc.getLatestBlockhash();
148+
const tx = buildAndSignTx(ixs, payer, blockhash, additionalSigners);
149+
return sendAndConfirmTx(rpc, tx, confirmOptions);
150+
}),
151+
);
90152
const { blockhash } = await rpc.getLatestBlockhash();
91153
const tx = buildAndSignTx(transferIxs, payer, blockhash, additionalSigners);
92154
return sendAndConfirmTx(rpc, tx, confirmOptions);
@@ -96,10 +158,76 @@ export interface TransferOptions extends InterfaceOptions {
96158
wrap?: boolean;
97159
programId?: PublicKey;
98160
}
161+
export type TransferToAccountOptions = TransferOptions;
99162

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

165+
export async function createTransferInterfaceInstructions(
166+
rpc: Rpc,
167+
payer: PublicKey,
168+
mint: PublicKey,
169+
amount: number | bigint | BN,
170+
sender: PublicKey,
171+
recipient: PublicKey,
172+
decimals: number,
173+
options?: TransferOptions,
174+
): Promise<TransactionInstruction[][]> {
175+
// Convenience path intentionally derives ATA from a wallet recipient.
176+
// PDA/off-curve recipients should use transferToAccountInterface with an
177+
// explicitly derived destination token account.
178+
const programId = options?.programId ?? LIGHT_TOKEN_PROGRAM_ID;
179+
const destination = getAssociatedTokenAddressInterface(
180+
mint,
181+
recipient,
182+
false,
183+
programId,
184+
);
185+
const batches = await createTransferToAccountInterfaceInstructions(
186+
rpc,
187+
payer,
188+
mint,
189+
amount,
190+
sender,
191+
destination,
192+
decimals,
193+
options,
194+
);
195+
196+
const ensureRecipientAtaIx =
197+
createAssociatedTokenAccountInterfaceIdempotentInstruction(
198+
payer,
199+
destination,
200+
recipient,
201+
mint,
202+
programId,
203+
);
204+
205+
const finalBatch = batches[batches.length - 1];
206+
let insertionIdx = 0;
207+
while (
208+
insertionIdx < finalBatch.length &&
209+
finalBatch[insertionIdx].programId.equals(
210+
ComputeBudgetProgram.programId,
211+
)
212+
) {
213+
insertionIdx += 1;
214+
}
215+
216+
const patchedFinalBatch = [
217+
...finalBatch.slice(0, insertionIdx),
218+
ensureRecipientAtaIx,
219+
...finalBatch.slice(insertionIdx),
220+
];
221+
const numSigners = payer.equals(sender) ? 1 : 2;
222+
assertTransactionSizeWithinLimit(
223+
patchedFinalBatch,
224+
numSigners,
225+
'Final transfer batch',
226+
);
227+
return [...batches.slice(0, -1), patchedFinalBatch];
228+
}
229+
102230
export {
103-
createTransferInterfaceInstructions,
231+
createTransferToAccountInterfaceInstructions,
104232
calculateTransferCU,
105233
} from '../instructions/transfer-interface';

js/compressed-token/src/v3/instructions/transfer-interface.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,15 @@ const LIGHT_TOKEN_TRANSFER_DISCRIMINATOR = 3;
3838
const LIGHT_TOKEN_TRANSFER_CHECKED_DISCRIMINATOR = 12;
3939

4040
const TRANSFER_BASE_CU = 10_000;
41+
const TRANSFER_EXTRA_BUFFER_CU = 10_000;
4142

4243
export function calculateTransferCU(
4344
loadBatch: InternalLoadBatch | null,
4445
): number {
45-
return calculateCombinedCU(TRANSFER_BASE_CU, loadBatch);
46+
return calculateCombinedCU(
47+
TRANSFER_BASE_CU + TRANSFER_EXTRA_BUFFER_CU,
48+
loadBatch,
49+
);
4650
}
4751

4852
/**
@@ -141,7 +145,7 @@ export function createLightTokenTransferCheckedInstruction(
141145
});
142146
}
143147

144-
export async function createTransferInterfaceInstructions(
148+
export async function createTransferToAccountInterfaceInstructions(
145149
rpc: Rpc,
146150
payer: PublicKey,
147151
mint: PublicKey,

0 commit comments

Comments
 (0)