Skip to content

Commit 2b9542d

Browse files
SwenSchaeferjohannSwen
andauthored
feat: lookuptable support + batch compress (#1087)
* add lut * cleanup * cleanup --------- Co-authored-by: Swen <swen.schaeferjohann@code.berlin>
1 parent 494570b commit 2b9542d

File tree

10 files changed

+516
-85
lines changed

10 files changed

+516
-85
lines changed

cli/src/commands/balance/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { Command, Flags } from "@oclif/core";
2-
import { CustomLoader, getSolanaRpcUrl, rpc } from "../../utils/utils";
2+
import { CustomLoader, rpc } from "../../utils/utils";
33
import { PublicKey } from "@solana/web3.js";
4-
import { getTestRpc } from "@lightprotocol/stateless.js";
5-
import { WasmFactory } from "@lightprotocol/hasher.rs";
64

75
class BalanceCommand extends Command {
86
static summary = "Get balance";

js/compressed-token/src/actions/compress.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
ComputeBudgetProgram,
77
} from '@solana/web3.js';
88
import {
9-
bn,
109
sendAndConfirmTx,
1110
buildAndSignTx,
1211
Rpc,
@@ -39,15 +38,13 @@ export async function compress(
3938
rpc: Rpc,
4039
payer: Signer,
4140
mint: PublicKey,
42-
amount: number | BN,
41+
amount: number | BN | number[] | BN[],
4342
owner: Signer,
4443
sourceTokenAccount: PublicKey,
45-
toAddress: PublicKey,
44+
toAddress: PublicKey | Array<PublicKey>,
4645
merkleTree?: PublicKey,
4746
confirmOptions?: ConfirmOptions,
4847
): Promise<TransactionSignature> {
49-
amount = bn(amount);
50-
5148
const compressIx = await CompressedTokenProgram.compress({
5249
payer: payer.publicKey,
5350
owner: owner.publicKey,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { PublicKey, Signer, TransactionSignature } from '@solana/web3.js';
2+
import {
3+
sendAndConfirmTx,
4+
buildAndSignTx,
5+
Rpc,
6+
dedupeSigner,
7+
} from '@lightprotocol/stateless.js';
8+
9+
import { CompressedTokenProgram } from '../program';
10+
11+
/**
12+
* Create a lookup table for the token program's default accounts
13+
*
14+
* @param rpc Rpc connection to use
15+
* @param payer Payer of the transaction fees
16+
* @param authority Authority of the lookup table
17+
* @param mints Optional array of mint public keys to include in
18+
* the lookup table
19+
* @param additionalAccounts Optional array of additional account public keys
20+
* to include in the lookup table
21+
*
22+
* @return Transaction signatures and the address of the created lookup table
23+
*/
24+
export async function createTokenProgramLookupTable(
25+
rpc: Rpc,
26+
payer: Signer,
27+
authority: Signer,
28+
mints?: PublicKey[],
29+
additionalAccounts?: PublicKey[],
30+
): Promise<{ txIds: TransactionSignature[]; address: PublicKey }> {
31+
const recentSlot = await rpc.getSlot('finalized');
32+
const { instructions, address } =
33+
await CompressedTokenProgram.createTokenProgramLookupTable({
34+
payer: payer.publicKey,
35+
authority: authority.publicKey,
36+
mints,
37+
remainingAccounts: additionalAccounts,
38+
recentSlot,
39+
});
40+
41+
const additionalSigners = dedupeSigner(payer, [authority]);
42+
const blockhashCtx = await rpc.getLatestBlockhash();
43+
const signedTx = buildAndSignTx(
44+
[instructions[0]],
45+
payer,
46+
blockhashCtx.blockhash,
47+
additionalSigners,
48+
);
49+
50+
/// Must wait for the first instruction to be finalized.
51+
const txId = await sendAndConfirmTx(
52+
rpc,
53+
signedTx,
54+
{ commitment: 'finalized' },
55+
blockhashCtx,
56+
);
57+
58+
const blockhashCtx2 = await rpc.getLatestBlockhash();
59+
const signedTx2 = buildAndSignTx(
60+
[instructions[1]],
61+
payer,
62+
blockhashCtx2.blockhash,
63+
additionalSigners,
64+
);
65+
const txId2 = await sendAndConfirmTx(
66+
rpc,
67+
signedTx2,
68+
{ commitment: 'finalized' },
69+
blockhashCtx2,
70+
);
71+
72+
return { txIds: [txId, txId2], address };
73+
}

js/compressed-token/src/actions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './create-mint';
55
export * from './mint-to';
66
export * from './register-mint';
77
export * from './transfer';
8+
export * from './create-token-program-lookup-table';

js/compressed-token/src/actions/mint-to.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import { CompressedTokenProgram } from '../program';
2020
* @param rpc Rpc to use
2121
* @param payer Payer of the transaction fees
2222
* @param mint Mint for the account
23-
* @param destination Address of the account to mint to
23+
* @param destination Address of the account to mint to. Can be an array of
24+
* addresses if the amount is an array of amounts.
2425
* @param authority Minting authority
25-
* @param amount Amount to mint
26+
* @param amount Amount to mint. Can be an array of amounts if the
27+
* destination is an array of addresses.
2628
* @param merkleTree State tree account that the compressed tokens should be
2729
* part of. Defaults to the default state tree account.
2830
* @param confirmOptions Options for confirming the transaction
@@ -33,9 +35,9 @@ export async function mintTo(
3335
rpc: Rpc,
3436
payer: Signer,
3537
mint: PublicKey,
36-
destination: PublicKey,
38+
destination: PublicKey | PublicKey[],
3739
authority: Signer,
38-
amount: number | BN,
40+
amount: number | BN | number[] | BN[],
3941
merkleTree?: PublicKey,
4042
confirmOptions?: ConfirmOptions,
4143
): Promise<TransactionSignature> {

js/compressed-token/src/program.ts

Lines changed: 134 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
TransactionInstruction,
55
SystemProgram,
66
Connection,
7+
AddressLookupTableProgram,
78
} from '@solana/web3.js';
89
import { BN, Program, AnchorProvider, setProvider } from '@coral-xyz/anchor';
910
import { IDL, LightCompressedToken } from './idl/light_compressed_token';
@@ -47,16 +48,17 @@ type CompressParams = {
4748
source: PublicKey;
4849
/**
4950
* owner of the compressed token account.
51+
* To compress to a batch of recipients, pass an array of PublicKeys.
5052
*/
51-
toAddress: PublicKey;
53+
toAddress: PublicKey | PublicKey[];
5254
/**
5355
* Mint address of the token to compress.
5456
*/
5557
mint: PublicKey;
5658
/**
5759
* amount of tokens to compress.
5860
*/
59-
amount: number | BN;
61+
amount: number | BN | number[] | BN[];
6062
/**
6163
* The state tree that the tx output should be inserted into. Defaults to a
6264
* public state tree if unspecified.
@@ -241,6 +243,29 @@ export type ApproveAndMintToParams = {
241243
merkleTree?: PublicKey;
242244
};
243245

246+
export type CreateTokenProgramLookupTableParams = {
247+
/**
248+
* The payer of the transaction.
249+
*/
250+
payer: PublicKey;
251+
/**
252+
* The authority of the transaction.
253+
*/
254+
authority: PublicKey;
255+
/**
256+
* Recently finalized Solana slot.
257+
*/
258+
recentSlot: number;
259+
/**
260+
* Optional Mint addresses to store in the lookup table.
261+
*/
262+
mints?: PublicKey[];
263+
/**
264+
* Optional additional addresses to store in the lookup table.
265+
*/
266+
remainingAccounts?: PublicKey[];
267+
};
268+
244269
/**
245270
* Sum up the token amounts of the compressed token accounts
246271
*/
@@ -513,6 +538,13 @@ export class CompressedTokenProgram {
513538
const amounts = toArray<BN | number>(amount).map(amount => bn(amount));
514539

515540
const toPubkeys = toArray(toPubkey);
541+
542+
if (amounts.length !== toPubkeys.length) {
543+
throw new Error(
544+
'Amount and toPubkey arrays must have the same length',
545+
);
546+
}
547+
516548
const instruction = await this.program.methods
517549
.mintTo(toPubkeys, amounts, null)
518550
.accounts({
@@ -658,24 +690,102 @@ export class CompressedTokenProgram {
658690
}
659691

660692
/**
661-
* Construct compress instruction
693+
* Create lookup table instructions for the token program's default accounts.
694+
*/
695+
static async createTokenProgramLookupTable(
696+
params: CreateTokenProgramLookupTableParams,
697+
) {
698+
const { authority, mints, recentSlot, payer, remainingAccounts } =
699+
params;
700+
701+
const [createInstruction, lookupTableAddress] =
702+
AddressLookupTableProgram.createLookupTable({
703+
authority,
704+
payer: authority,
705+
recentSlot,
706+
});
707+
708+
let optionalMintKeys: PublicKey[] = [];
709+
if (mints) {
710+
optionalMintKeys = [
711+
...mints,
712+
...mints.map(mint => this.deriveTokenPoolPda(mint)),
713+
];
714+
}
715+
716+
const extendInstruction = AddressLookupTableProgram.extendLookupTable({
717+
payer,
718+
authority,
719+
lookupTable: lookupTableAddress,
720+
addresses: [
721+
this.deriveCpiAuthorityPda,
722+
LightSystemProgram.programId,
723+
defaultStaticAccountsStruct().registeredProgramPda,
724+
defaultStaticAccountsStruct().noopProgram,
725+
defaultStaticAccountsStruct().accountCompressionAuthority,
726+
defaultStaticAccountsStruct().accountCompressionProgram,
727+
defaultTestStateTreeAccounts().merkleTree,
728+
defaultTestStateTreeAccounts().nullifierQueue,
729+
defaultTestStateTreeAccounts().addressTree,
730+
defaultTestStateTreeAccounts().addressQueue,
731+
this.programId,
732+
TOKEN_PROGRAM_ID,
733+
authority,
734+
...optionalMintKeys,
735+
...(remainingAccounts ?? []),
736+
],
737+
});
738+
739+
return {
740+
instructions: [createInstruction, extendInstruction],
741+
address: lookupTableAddress,
742+
};
743+
}
744+
745+
/**
746+
* Create compress instruction
662747
* @returns compressInstruction
663748
*/
664749
static async compress(
665750
params: CompressParams,
666751
): Promise<TransactionInstruction> {
667752
const { payer, owner, source, toAddress, mint, outputStateTree } =
668753
params;
669-
const amount = bn(params.amount);
670754

671-
const tokenTransferOutputs: TokenTransferOutputData[] = [
672-
{
673-
owner: toAddress,
674-
amount,
675-
lamports: bn(0),
676-
tlv: null,
677-
},
678-
];
755+
if (Array.isArray(params.amount) !== Array.isArray(params.toAddress)) {
756+
throw new Error(
757+
'Both amount and toAddress must be arrays or both must be single values',
758+
);
759+
}
760+
761+
let tokenTransferOutputs: TokenTransferOutputData[];
762+
763+
if (Array.isArray(params.amount) && Array.isArray(params.toAddress)) {
764+
if (params.amount.length !== params.toAddress.length) {
765+
throw new Error(
766+
'Amount and toAddress arrays must have the same length',
767+
);
768+
}
769+
tokenTransferOutputs = params.amount.map((amt, index) => {
770+
const amount = bn(amt);
771+
return {
772+
owner: (params.toAddress as PublicKey[])[index],
773+
amount,
774+
lamports: bn(0),
775+
tlv: null,
776+
};
777+
});
778+
} else {
779+
tokenTransferOutputs = [
780+
{
781+
owner: toAddress as PublicKey,
782+
amount: bn(params.amount as number | BN),
783+
lamports: bn(0),
784+
tlv: null,
785+
},
786+
];
787+
}
788+
679789
const {
680790
inputTokenDataWithContext,
681791
packedOutputTokenData,
@@ -693,7 +803,11 @@ export class CompressedTokenProgram {
693803
delegatedTransfer: null, // TODO: implement
694804
inputTokenDataWithContext,
695805
outputCompressedAccounts: packedOutputTokenData,
696-
compressOrDecompressAmount: amount,
806+
compressOrDecompressAmount: Array.isArray(params.amount)
807+
? params.amount
808+
.map(amt => new BN(amt))
809+
.reduce((sum, amt) => sum.add(amt), new BN(0))
810+
: new BN(params.amount),
697811
isCompress: true,
698812
cpiContext: null,
699813
lamportsChangeAccountMerkleTreeIndex: null,
@@ -704,24 +818,20 @@ export class CompressedTokenProgram {
704818
data,
705819
);
706820

707-
const {
708-
accountCompressionAuthority,
709-
noopProgram,
710-
registeredProgramPda,
711-
accountCompressionProgram,
712-
} = defaultStaticAccountsStruct();
713-
714821
const instruction = await this.program.methods
715822
.transfer(encodedData)
716823
.accounts({
717824
feePayer: payer,
718825
authority: owner,
719826
cpiAuthorityPda: this.deriveCpiAuthorityPda,
720827
lightSystemProgram: LightSystemProgram.programId,
721-
registeredProgramPda: registeredProgramPda,
722-
noopProgram: noopProgram,
723-
accountCompressionAuthority: accountCompressionAuthority,
724-
accountCompressionProgram: accountCompressionProgram,
828+
registeredProgramPda:
829+
defaultStaticAccountsStruct().registeredProgramPda,
830+
noopProgram: defaultStaticAccountsStruct().noopProgram,
831+
accountCompressionAuthority:
832+
defaultStaticAccountsStruct().accountCompressionAuthority,
833+
accountCompressionProgram:
834+
defaultStaticAccountsStruct().accountCompressionProgram,
725835
selfProgram: this.programId,
726836
tokenPoolPda: this.deriveTokenPoolPda(mint),
727837
compressOrDecompressTokenAccount: source, // token

0 commit comments

Comments
 (0)