Skip to content

Commit 9b2ff26

Browse files
add mint-to-compressed
1 parent 5551198 commit 9b2ff26

5 files changed

Lines changed: 381 additions & 0 deletions

File tree

js/token-interface/src/instructions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export {
2525
createBurnInstructions,
2626
createBurnInstructionPlan,
2727
} from './burn';
28+
export { createMintToCompressedInstruction } from './mint-to-compressed';
2829
export {
2930
createFreezeInstruction,
3031
createFreezeInstructions,
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import {
2+
SystemProgram,
3+
TransactionInstruction,
4+
} from '@solana/web3.js';
5+
import { Buffer } from 'buffer';
6+
import {
7+
LIGHT_TOKEN_PROGRAM_ID,
8+
LightSystemProgram,
9+
defaultStaticAccountsStruct,
10+
getOutputQueue,
11+
} from '@lightprotocol/stateless.js';
12+
import {
13+
MAX_TOP_UP,
14+
TokenDataVersion,
15+
deriveCpiAuthorityPda,
16+
} from '../constants';
17+
import {
18+
MintActionCompressedInstructionData,
19+
encodeMintActionInstructionData,
20+
} from './layout/layout-mint-action';
21+
import type { CreateRawMintToCompressedInstructionInput } from '../types';
22+
23+
/**
24+
* Create instruction for minting from a light mint directly to compressed token accounts.
25+
*/
26+
export function createMintToCompressedInstruction({
27+
authority,
28+
payer,
29+
validityProof,
30+
merkleContext,
31+
mintData,
32+
recipients,
33+
outputStateTreeInfo,
34+
tokenAccountVersion = TokenDataVersion.ShaFlat,
35+
maxTopUp,
36+
}: CreateRawMintToCompressedInstructionInput): TransactionInstruction {
37+
if (mintData.metadata) {
38+
throw new Error(
39+
'TokenMetadata extension not supported in mintToCompressed instruction',
40+
);
41+
}
42+
if (validityProof.rootIndices.length === 0) {
43+
throw new Error('Missing root index for mintToCompressed instruction.');
44+
}
45+
46+
const isDecompressed = mintData.mintDecompressed;
47+
const mintSigner = Array.from(mintData.mintSigner);
48+
const instructionData: MintActionCompressedInstructionData = {
49+
leafIndex: merkleContext.leafIndex,
50+
proveByIndex: true,
51+
rootIndex: validityProof.rootIndices[0],
52+
maxTopUp: maxTopUp ?? MAX_TOP_UP,
53+
createMint: null,
54+
actions: [
55+
{
56+
mintToCompressed: {
57+
tokenAccountVersion,
58+
recipients: recipients.map(recipient => ({
59+
recipient: recipient.recipient,
60+
amount: BigInt(recipient.amount.toString()),
61+
})),
62+
},
63+
},
64+
],
65+
proof: isDecompressed ? null : validityProof.compressedProof,
66+
cpiContext: null,
67+
mint: isDecompressed
68+
? null
69+
: {
70+
supply: mintData.supply,
71+
decimals: mintData.decimals,
72+
metadata: {
73+
version: mintData.version,
74+
cmintDecompressed: mintData.mintDecompressed,
75+
mint: mintData.splMint,
76+
mintSigner,
77+
bump: mintData.bump,
78+
},
79+
mintAuthority: mintData.mintAuthority,
80+
freezeAuthority: mintData.freezeAuthority,
81+
extensions: null,
82+
},
83+
};
84+
85+
const outputQueue =
86+
outputStateTreeInfo?.queue ?? getOutputQueue(merkleContext);
87+
const sys = defaultStaticAccountsStruct();
88+
89+
return new TransactionInstruction({
90+
programId: LIGHT_TOKEN_PROGRAM_ID,
91+
keys: [
92+
{
93+
pubkey: LightSystemProgram.programId,
94+
isSigner: false,
95+
isWritable: false,
96+
},
97+
{ pubkey: authority, isSigner: true, isWritable: false },
98+
...(isDecompressed
99+
? [
100+
{
101+
pubkey: mintData.splMint,
102+
isSigner: false,
103+
isWritable: true,
104+
},
105+
]
106+
: []),
107+
{ pubkey: payer, isSigner: true, isWritable: true },
108+
{
109+
pubkey: deriveCpiAuthorityPda(),
110+
isSigner: false,
111+
isWritable: false,
112+
},
113+
{
114+
pubkey: sys.registeredProgramPda,
115+
isSigner: false,
116+
isWritable: false,
117+
},
118+
{
119+
pubkey: sys.accountCompressionAuthority,
120+
isSigner: false,
121+
isWritable: false,
122+
},
123+
{
124+
pubkey: sys.accountCompressionProgram,
125+
isSigner: false,
126+
isWritable: false,
127+
},
128+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
129+
{ pubkey: outputQueue, isSigner: false, isWritable: true },
130+
{ pubkey: merkleContext.treeInfo.tree, isSigner: false, isWritable: true },
131+
{
132+
pubkey: merkleContext.treeInfo.queue,
133+
isSigner: false,
134+
isWritable: true,
135+
},
136+
{ pubkey: outputQueue, isSigner: false, isWritable: true },
137+
],
138+
data: encodeMintActionInstructionData(instructionData),
139+
});
140+
}

js/token-interface/src/types.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import type {
22
AddressTreeInfo,
3+
MerkleContext,
34
ParsedTokenAccount,
45
Rpc,
56
TreeInfo,
67
ValidityProofWithContext,
78
} from '@lightprotocol/stateless.js';
89
import type { Commitment, PublicKey, Signer } from '@solana/web3.js';
10+
import type { TokenDataVersion } from './constants';
911

1012
export interface TokenInterfaceParsedAta {
1113
address: PublicKey;
@@ -215,3 +217,33 @@ export interface CreateRawLightMintInstructionInput {
215217
tokenMetadata?: TokenMetadataInput;
216218
maxTopUp?: number;
217219
}
220+
221+
export interface MintToCompressedMintData {
222+
supply: bigint;
223+
decimals: number;
224+
mintAuthority: PublicKey | null;
225+
freezeAuthority: PublicKey | null;
226+
splMint: PublicKey;
227+
mintDecompressed: boolean;
228+
version: number;
229+
mintSigner: Uint8Array | number[];
230+
bump: number;
231+
metadata?: {
232+
updateAuthority: PublicKey | null;
233+
name: string;
234+
symbol: string;
235+
uri: string;
236+
};
237+
}
238+
239+
export interface CreateRawMintToCompressedInstructionInput {
240+
authority: PublicKey;
241+
payer: PublicKey;
242+
validityProof: ValidityProofWithContext;
243+
merkleContext: MerkleContext;
244+
mintData: MintToCompressedMintData;
245+
recipients: Array<{ recipient: PublicKey; amount: number | bigint }>;
246+
outputStateTreeInfo?: TreeInfo;
247+
tokenAccountVersion?: TokenDataVersion;
248+
maxTopUp?: number;
249+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { Keypair, PublicKey } from '@solana/web3.js';
3+
import { Buffer } from 'buffer';
4+
import {
5+
DerivationMode,
6+
LIGHT_TOKEN_PROGRAM_ID,
7+
VERSION,
8+
bn,
9+
createRpc,
10+
featureFlags,
11+
newAccountWithLamports,
12+
selectStateTreeInfo,
13+
} from '@lightprotocol/stateless.js';
14+
import {
15+
createMintInstructions,
16+
createMintToCompressedInstruction,
17+
getMint,
18+
} from '../../src';
19+
import { sendInstructions } from './helpers';
20+
21+
featureFlags.version = VERSION.V2;
22+
const COMPRESSED_MINT_SEED = Buffer.from('compressed_mint');
23+
24+
describe('mint-to-compressed instruction', () => {
25+
it('mints directly to compressed recipients for a light mint', async () => {
26+
const rpc = createRpc();
27+
const payer = await newAccountWithLamports(rpc, 20e9);
28+
const mintAuthority = Keypair.generate();
29+
const mintSigner = Keypair.generate();
30+
const recipientA = Keypair.generate();
31+
const recipientB = Keypair.generate();
32+
const outputStateTreeInfo = selectStateTreeInfo(
33+
await rpc.getStateTreeInfos(),
34+
);
35+
36+
await sendInstructions(
37+
rpc,
38+
payer,
39+
await createMintInstructions({
40+
rpc,
41+
payer: payer.publicKey,
42+
keypair: mintSigner,
43+
decimals: 9,
44+
mintAuthority: mintAuthority.publicKey,
45+
tokenProgramId: LIGHT_TOKEN_PROGRAM_ID,
46+
outputStateTreeInfo,
47+
}),
48+
[mintSigner, mintAuthority],
49+
);
50+
51+
const [mint] = PublicKey.findProgramAddressSync(
52+
[COMPRESSED_MINT_SEED, mintSigner.publicKey.toBuffer()],
53+
LIGHT_TOKEN_PROGRAM_ID,
54+
);
55+
const mintInfo = await getMint(rpc, mint, undefined, LIGHT_TOKEN_PROGRAM_ID);
56+
if (!mintInfo.merkleContext || !mintInfo.mintContext) {
57+
throw new Error('Light mint context missing.');
58+
}
59+
60+
const validityProof = await rpc.getValidityProofV2(
61+
[
62+
{
63+
hash: bn(mintInfo.merkleContext.hash),
64+
leafIndex: mintInfo.merkleContext.leafIndex,
65+
treeInfo: mintInfo.merkleContext.treeInfo,
66+
proveByIndex: mintInfo.merkleContext.proveByIndex,
67+
},
68+
],
69+
[],
70+
DerivationMode.compressible,
71+
);
72+
73+
const ix = createMintToCompressedInstruction({
74+
authority: mintAuthority.publicKey,
75+
payer: payer.publicKey,
76+
validityProof,
77+
merkleContext: mintInfo.merkleContext,
78+
mintData: {
79+
supply: mintInfo.mint.supply,
80+
decimals: mintInfo.mint.decimals,
81+
mintAuthority: mintInfo.mint.mintAuthority,
82+
freezeAuthority: mintInfo.mint.freezeAuthority,
83+
splMint: mintInfo.mintContext.splMint,
84+
mintDecompressed: mintInfo.mintContext.cmintDecompressed,
85+
version: mintInfo.mintContext.version,
86+
mintSigner: mintInfo.mintContext.mintSigner,
87+
bump: mintInfo.mintContext.bump,
88+
},
89+
recipients: [
90+
{ recipient: recipientA.publicKey, amount: 500n },
91+
{ recipient: recipientB.publicKey, amount: 700n },
92+
],
93+
outputStateTreeInfo,
94+
});
95+
96+
await sendInstructions(rpc, payer, [ix], [mintAuthority]);
97+
98+
const aAccounts = await rpc.getCompressedTokenAccountsByOwner(
99+
recipientA.publicKey,
100+
{ mint },
101+
);
102+
const bAccounts = await rpc.getCompressedTokenAccountsByOwner(
103+
recipientB.publicKey,
104+
{ mint },
105+
);
106+
const mintAfter = await getMint(rpc, mint, undefined, LIGHT_TOKEN_PROGRAM_ID);
107+
108+
const amountA = aAccounts.items.reduce(
109+
(sum, account) => sum + BigInt(account.parsed.amount.toString()),
110+
0n,
111+
);
112+
const amountB = bAccounts.items.reduce(
113+
(sum, account) => sum + BigInt(account.parsed.amount.toString()),
114+
0n,
115+
);
116+
117+
expect(amountA).toBe(500n);
118+
expect(amountB).toBe(700n);
119+
expect(mintAfter.mint.supply).toBe(1_200n);
120+
});
121+
});

0 commit comments

Comments
 (0)