diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index c28dae0d6f..b903b862e0 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -65,7 +65,6 @@ "@solana/web3.js": ">=1.95.5" }, "dependencies": { - "@coral-xyz/anchor": "0.29.0", "@coral-xyz/borsh": "^0.29.0", "@solana/spl-token": "0.4.8", "bn.js": "^5.2.1", diff --git a/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts b/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts index 5e6b279d11..2a15d388ab 100644 --- a/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts +++ b/js/compressed-token/src/instructions/pack-compressed-token-accounts.ts @@ -4,10 +4,12 @@ import { getIndexOrAdd, bn, padOutputStateMerkleTrees, - TokenTransferOutputData, } from '@lightprotocol/stateless.js'; import { PublicKey, AccountMeta } from '@solana/web3.js'; -import { PackedTokenTransferOutputData } from '../types'; +import { + PackedTokenTransferOutputData, + TokenTransferOutputData, +} from '../types'; export type PackCompressedTokenAccountsParams = { /** Input state to be consumed */ diff --git a/js/compressed-token/src/layout.ts b/js/compressed-token/src/layout.ts index bf5645ea23..e3a890cb2b 100644 --- a/js/compressed-token/src/layout.ts +++ b/js/compressed-token/src/layout.ts @@ -13,10 +13,13 @@ import { } from '@coral-xyz/borsh'; import { Buffer } from 'buffer'; -import { CompressedTokenInstructionDataTransfer } from '@lightprotocol/stateless.js'; import { AccountMeta, PublicKey } from '@solana/web3.js'; import { CompressedTokenProgram } from './program'; import BN from 'bn.js'; +import { + CompressedCpiContext, + CompressedTokenInstructionDataTransfer, +} from './types'; export const CREATE_TOKEN_POOL_DISCRIMINATOR = Buffer.from([ 23, 169, 27, 122, 147, 169, 209, 152, @@ -27,13 +30,16 @@ const CompressedProofLayout = struct([ array(u8(), 32, 'c'), ]); -const TokenTransferOutputDataLayout = struct([ +const PackedTokenTransferOutputDataLayout = struct([ publicKey('owner'), u64('amount'), option(u64(), 'lamports'), + u8('merkleTreeIndex'), option(vecU8(), 'tlv'), ]); +const QueueIndexLayout = struct([u8('queueId'), u16('index')]); + const InputTokenDataWithContextLayout = struct([ u64('amount'), option(u8(), 'delegateIndex'), @@ -42,7 +48,7 @@ const InputTokenDataWithContextLayout = struct([ u8('merkleTreePubkeyIndex'), u8('nullifierQueuePubkeyIndex'), u32('leafIndex'), - option(struct([u8('queueId'), u16('index')]), 'queueIndex'), + option(QueueIndexLayout, 'QueueIndex'), ], 'merkleContext', ), @@ -50,6 +56,7 @@ const InputTokenDataWithContextLayout = struct([ option(u64(), 'lamports'), option(vecU8(), 'tlv'), ]); + export const DelegatedTransferLayout = struct([ publicKey('owner'), option(u8(), 'delegateChangeAccountIndex'), @@ -65,11 +72,8 @@ export const CompressedTokenInstructionDataTransferLayout = struct([ option(CompressedProofLayout, 'proof'), publicKey('mint'), option(DelegatedTransferLayout, 'delegatedTransfer'), - vec( - InputTokenDataWithContextLayout, - 'inputCompressedAccountsWithMerkleContext', - ), - vec(TokenTransferOutputDataLayout, 'outputCompressedAccounts'), + vec(InputTokenDataWithContextLayout, 'inputTokenDataWithContext'), + vec(PackedTokenTransferOutputDataLayout, 'outputCompressedAccounts'), bool('isCompress'), option(u64(), 'compressOrDecompressAmount'), option(CpiContextLayout, 'cpiContext'), @@ -82,9 +86,18 @@ export const mintToLayout = struct([ option(u64(), 'lamports'), ]); +export const compressSplTokenAccountInstructionDataLayout = struct([ + publicKey('owner'), + option(u64(), 'remainingAmount'), + option(CpiContextLayout, 'cpiContext'), +]); + const MINT_TO_DISCRIMINATOR = Buffer.from([ 241, 34, 48, 186, 37, 179, 123, 192, ]); +export const TRANSFER_DISCRIMINATOR = Buffer.from([ + 163, 52, 200, 231, 140, 3, 69, 186, +]); export function encodeMintToInstructionData( recipients: PublicKey[], amounts: BN[], @@ -102,17 +115,40 @@ export function encodeMintToInstructionData( return Buffer.concat([MINT_TO_DISCRIMINATOR, buffer.slice(0, len)]); } +export const COMPRESS_SPL_TOKEN_ACCOUNT_DISCRIMINATOR = Buffer.from([ + 112, 230, 105, 101, 145, 202, 157, 97, +]); + +export function encodeCompressSplTokenAccountInstructionData( + owner: PublicKey, + remainingAmount: BN | null, + cpiContext: CompressedCpiContext | null, +): Buffer { + const buffer = Buffer.alloc(1000); + const len = compressSplTokenAccountInstructionDataLayout.encode( + { + owner, + remainingAmount, + cpiContext, + }, + buffer, + ); + return Buffer.concat([ + COMPRESS_SPL_TOKEN_ACCOUNT_DISCRIMINATOR, + buffer.slice(0, len), + ]); +} + export function encodeCompressedTokenInstructionDataTransfer( data: CompressedTokenInstructionDataTransfer, ): Buffer { const buffer = Buffer.alloc(1000); - console.log('DATA raw:', data); + const len = CompressedTokenInstructionDataTransferLayout.encode( data, buffer, ); - console.log('DATA len:', len); - // console.log('DATA buffer:', Array.from(buffer)); + const lengthBuffer = Buffer.alloc(4); lengthBuffer.writeUInt32LE(len, 0); @@ -122,9 +158,6 @@ export function encodeCompressedTokenInstructionDataTransfer( buffer.slice(0, len), ]); } -export const TRANSFER_DISCRIMINATOR = Buffer.from([ - 163, 52, 200, 231, 140, 3, 69, 186, -]); export type createTokenPoolAccountsLayoutParams = { feePayer: PublicKey; diff --git a/js/compressed-token/src/program.ts b/js/compressed-token/src/program.ts index dccc317c8f..f77da8a55c 100644 --- a/js/compressed-token/src/program.ts +++ b/js/compressed-token/src/program.ts @@ -1,6 +1,5 @@ import { PublicKey, - Keypair, TransactionInstruction, SystemProgram, Connection, @@ -8,18 +7,14 @@ import { AccountMeta, } from '@solana/web3.js'; import BN from 'bn.js'; -import { Program, AnchorProvider, setProvider } from '@coral-xyz/anchor'; -import { IDL, LightCompressedToken } from './idl/light_compressed_token'; import { CompressedProof, LightSystemProgram, ParsedTokenAccount, bn, - confirmConfig, defaultStaticAccountsStruct, sumUpLamports, toArray, - useWallet, validateSameOwner, validateSufficientBalance, defaultTestStateTreeAccounts, @@ -37,11 +32,15 @@ import { CREATE_TOKEN_POOL_DISCRIMINATOR, createTokenPoolAccountsLayout, encodeCompressedTokenInstructionDataTransfer, + encodeCompressSplTokenAccountInstructionData, encodeMintToInstructionData, mintToAccountsLayout, transferAccountsLayout, } from './layout'; -import { CompressedTokenInstructionDataTransfer, TokenTransferOutputData } from './types'; +import { + CompressedTokenInstructionDataTransfer, + TokenTransferOutputData, +} from './types'; export type CompressParams = { /** @@ -526,42 +525,6 @@ export class CompressedTokenProgram { typeof programId === 'string' ? new PublicKey(programId) : programId; - // Reset program when programId changes - this._program = null; - } - - private static _program: Program | null = null; - - /** @internal */ - static get program(): Program { - if (!this._program) { - this.initializeProgram(); - } - return this._program!; - } - - /** - * @internal - * Initializes the program statically if not already initialized. - */ - private static initializeProgram() { - if (!this._program) { - /// Note: We can use a mock connection because we're using the - /// program only for serde and building instructions, not for - /// interacting with the network. - const mockKeypair = Keypair.generate(); - const mockConnection = new Connection( - 'http://127.0.0.1:8899', - 'confirmed', - ); - const mockProvider = new AnchorProvider( - mockConnection, - useWallet(mockKeypair), - confirmConfig, - ); - setProvider(mockProvider); - this._program = new Program(IDL, this.programId, mockProvider); - } } /** @internal */ @@ -794,7 +757,7 @@ export class CompressedTokenProgram { inputCompressedTokenAccounts, ); - const data: CompressedTokenInstructionDataTransfer = { + const rawData: CompressedTokenInstructionDataTransfer = { proof: recentValidityProof, mint, delegatedTransfer: null, // TODO: implement @@ -806,14 +769,7 @@ export class CompressedTokenProgram { lamportsChangeAccountMerkleTreeIndex: null, }; - const encodedData = this.program.coder.types.encode( - 'CompressedTokenInstructionDataTransfer', - data, - ); - - console.log('Encoded data:', encodedData); - console.log('First 8 bytes:', encodedData.slice(0, 8)); - console.log('Bytes 8-14:', encodedData.slice(8, 14)); + const data = encodeCompressedTokenInstructionDataTransfer(rawData); const { accountCompressionAuthority, @@ -821,27 +777,29 @@ export class CompressedTokenProgram { registeredProgramPda, accountCompressionProgram, } = defaultStaticAccountsStruct(); + const keys = transferAccountsLayout({ + feePayer: payer, + authority: currentOwner, + cpiAuthorityPda: this.deriveCpiAuthorityPda, + lightSystemProgram: LightSystemProgram.programId, + registeredProgramPda: registeredProgramPda, + noopProgram: noopProgram, + accountCompressionAuthority: accountCompressionAuthority, + accountCompressionProgram: accountCompressionProgram, + selfProgram: this.programId, + tokenPoolPda: undefined, + compressOrDecompressTokenAccount: undefined, + tokenProgram: undefined, + systemProgram: SystemProgram.programId, + }); + + keys.push(...remainingAccountMetas); - const instruction = await this.program.methods - .transfer(encodedData) - .accounts({ - feePayer: payer!, - authority: currentOwner!, - cpiAuthorityPda: this.deriveCpiAuthorityPda, - lightSystemProgram: LightSystemProgram.programId, - registeredProgramPda: registeredProgramPda, - noopProgram: noopProgram, - accountCompressionAuthority: accountCompressionAuthority, - accountCompressionProgram: accountCompressionProgram, - selfProgram: this.programId, - tokenPoolPda: null, - compressOrDecompressTokenAccount: null, - tokenProgram: null, - }) - .remainingAccounts(remainingAccountMetas) - .instruction(); - - return instruction; + return new TransactionInstruction({ + programId: this.programId, + keys, + data, + }); } /** @@ -977,11 +935,6 @@ export class CompressedTokenProgram { }; const data = encodeCompressedTokenInstructionDataTransfer(rawData); - console.log('encoded data manually:', Array.from(data)); - const encodedData2 = this.program.coder.types.encode( - 'CompressedTokenInstructionDataTransfer', - rawData, - ); const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; @@ -998,36 +951,13 @@ export class CompressedTokenProgram { tokenProgram, }); - const instruction = new TransactionInstruction({ + keys.push(...remainingAccountMetas); + + return new TransactionInstruction({ programId: this.programId, keys, data, }); - - const instruction2 = await this.program.methods - .transfer(encodedData2) - .accounts({ - feePayer: payer, - authority: owner, - cpiAuthorityPda: this.deriveCpiAuthorityPda, - lightSystemProgram: LightSystemProgram.programId, - registeredProgramPda: - defaultStaticAccountsStruct().registeredProgramPda, - noopProgram: defaultStaticAccountsStruct().noopProgram, - accountCompressionAuthority: - defaultStaticAccountsStruct().accountCompressionAuthority, - accountCompressionProgram: - defaultStaticAccountsStruct().accountCompressionProgram, - selfProgram: this.programId, - tokenPoolPda: this.deriveTokenPoolPda(mint), - compressOrDecompressTokenAccount: source, // token - tokenProgram, - }) - .remainingAccounts(remainingAccountMetas) - .instruction(); - console.log('encoded data auto:', Array.from(instruction2.data)); - - return instruction; } /** @@ -1068,7 +998,7 @@ export class CompressedTokenProgram { inputCompressedTokenAccounts, ); - const data: CompressedTokenInstructionDataTransfer = { + const rawData: CompressedTokenInstructionDataTransfer = { proof: recentValidityProof, mint, delegatedTransfer: null, // TODO: implement @@ -1079,12 +1009,8 @@ export class CompressedTokenProgram { cpiContext: null, lamportsChangeAccountMerkleTreeIndex: null, }; - - const encodedData = this.program.coder.types.encode( - 'CompressedTokenInstructionDataTransfer', - data, - ); - + const data = encodeCompressedTokenInstructionDataTransfer(rawData); + const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; const { accountCompressionAuthority, noopProgram, @@ -1092,28 +1018,29 @@ export class CompressedTokenProgram { accountCompressionProgram, } = defaultStaticAccountsStruct(); - const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ID; + const keys = transferAccountsLayout({ + feePayer: payer, + authority: currentOwner, + cpiAuthorityPda: this.deriveCpiAuthorityPda, + lightSystemProgram: LightSystemProgram.programId, + registeredProgramPda: registeredProgramPda, + noopProgram: noopProgram, + accountCompressionAuthority: accountCompressionAuthority, + accountCompressionProgram: accountCompressionProgram, + selfProgram: this.programId, + tokenPoolPda: this.deriveTokenPoolPda(mint), + compressOrDecompressTokenAccount: toAddress, + tokenProgram, + systemProgram: SystemProgram.programId, + }); + + keys.push(...remainingAccountMetas); - const instruction = await this.program.methods - .transfer(encodedData) - .accounts({ - feePayer: payer, - authority: currentOwner, - cpiAuthorityPda: this.deriveCpiAuthorityPda, - lightSystemProgram: LightSystemProgram.programId, - registeredProgramPda: registeredProgramPda, - noopProgram: noopProgram, - accountCompressionAuthority: accountCompressionAuthority, - accountCompressionProgram: accountCompressionProgram, - selfProgram: this.programId, - tokenPoolPda: this.deriveTokenPoolPda(mint), - compressOrDecompressTokenAccount: toAddress, - tokenProgram, - }) - .remainingAccounts(remainingAccountMetas) - .instruction(); - - return instruction; + return new TransactionInstruction({ + programId: this.programId, + keys, + data, + }); } static async mergeTokenAccounts( @@ -1170,30 +1097,40 @@ export class CompressedTokenProgram { }, ]; - const instruction = await this.program.methods - .compressSplTokenAccount(authority, remainingAmount ?? null, null) - .accounts({ - feePayer, - authority, - cpiAuthorityPda: this.deriveCpiAuthorityPda, - lightSystemProgram: LightSystemProgram.programId, - registeredProgramPda: - defaultStaticAccountsStruct().registeredProgramPda, - noopProgram: defaultStaticAccountsStruct().noopProgram, - accountCompressionAuthority: - defaultStaticAccountsStruct().accountCompressionAuthority, - accountCompressionProgram: - defaultStaticAccountsStruct().accountCompressionProgram, - selfProgram: this.programId, - tokenPoolPda: this.deriveTokenPoolPda(mint), - compressOrDecompressTokenAccount: tokenAccount, - tokenProgram, - systemProgram: SystemProgram.programId, - }) - .remainingAccounts(remainingAccountMetas) - .instruction(); - - return instruction; + const data = encodeCompressSplTokenAccountInstructionData( + authority, + remainingAmount ?? null, + null, + ); + const { + accountCompressionAuthority, + noopProgram, + registeredProgramPda, + accountCompressionProgram, + } = defaultStaticAccountsStruct(); + const keys = transferAccountsLayout({ + feePayer, + authority, + cpiAuthorityPda: this.deriveCpiAuthorityPda, + lightSystemProgram: LightSystemProgram.programId, + registeredProgramPda: registeredProgramPda, + noopProgram: noopProgram, + accountCompressionAuthority: accountCompressionAuthority, + accountCompressionProgram: accountCompressionProgram, + selfProgram: this.programId, + tokenPoolPda: this.deriveTokenPoolPda(mint), + compressOrDecompressTokenAccount: tokenAccount, + tokenProgram, + systemProgram: SystemProgram.programId, + }); + + keys.push(...remainingAccountMetas); + + return new TransactionInstruction({ + programId: this.programId, + keys, + data, + }); } static async get_mint_program_id( diff --git a/js/compressed-token/src/types.ts b/js/compressed-token/src/types.ts index 77a55d0e15..8247dcee25 100644 --- a/js/compressed-token/src/types.ts +++ b/js/compressed-token/src/types.ts @@ -1,7 +1,15 @@ import { PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; -import { CompressedProof } from '@lightprotocol/stateless.js'; +import { + CompressedProof, + PackedMerkleContext, +} from '@lightprotocol/stateless.js'; +export type CompressedCpiContext = { + setContext: boolean; + firstSetContext: boolean; + cpiContextAccountIndex: number; // u8 +}; /// TODO: remove index_mt_account on-chain. passed as part of /// CompressedTokenInstructionDataInvoke export type TokenTransferOutputData = { @@ -47,45 +55,24 @@ export type PackedTokenTransferOutputData = { }; export type InputTokenDataWithContext = { - /** - * The amount of tokens to transfer - */ amount: BN; - /** - * Optional: The index of the delegate in remaining accounts - */ delegateIndex: number | null; - // /** - // * The index of the merkle tree address in remaining accounts - // */ - // merkleTreePubkeyIndex: number; - // /** - // * The index of the nullifier queue address in remaining accounts - // */ - // nullifierQueuePubkeyIndex: number; - /** - * The index of the leaf in the merkle tree - */ - // leafIndex: number; - /** - * Lamports in the input token account. - */ + merkleContext: PackedMerkleContext; + rootIndex: number; lamports: BN | null; - /** - * TokenExtension tlv - */ tlv: Buffer | null; }; +export type DelegatedTransfer = { + owner: PublicKey; + delegateChangeAccountIndex: number | null; +}; + export type CompressedTokenInstructionDataTransfer = { /** * Validity proof */ proof: CompressedProof | null; - /** - * The root indices of the transfer - */ - rootIndices: number[]; /** * The mint of the transfer */ @@ -94,7 +81,7 @@ export type CompressedTokenInstructionDataTransfer = { * Whether the signer is a delegate * TODO: implement delegated transfer struct */ - delegatedTransfer: null; + delegatedTransfer: DelegatedTransfer | null; /** * Input token data with packed merkle context */ @@ -104,10 +91,18 @@ export type CompressedTokenInstructionDataTransfer = { */ outputCompressedAccounts: PackedTokenTransferOutputData[]; /** - * The indices of the output state merkle tree accounts in 'remaining - * accounts' + * Whether it's a compress or decompress action if compressOrDecompressAmount is non-null + */ + isCompress: boolean; + /** + * If null, it's a transfer. + * If some, the amount that is being deposited into (compress) or withdrawn from (decompress) the token escrow + */ + compressOrDecompressAmount: BN | null; + /** + * CPI context if */ - outputStateMerkleTreeAccountIndices: Buffer; + cpiContext: CompressedCpiContext | null; /** * The index of the Merkle tree for a lamport change account. */ diff --git a/js/compressed-token/tests/e2e/compress.test.ts b/js/compressed-token/tests/e2e/compress.test.ts index e1c366404b..5ae12b879a 100644 --- a/js/compressed-token/tests/e2e/compress.test.ts +++ b/js/compressed-token/tests/e2e/compress.test.ts @@ -136,7 +136,7 @@ describe('compress', () => { lut = address; }, 80_000); - it.only('should compress from bobAta -> charlie', async () => { + it('should compress from bobAta -> charlie', async () => { const senderAtaBalanceBefore = await rpc.getTokenAccountBalance(bobAta); const recipientCompressedTokenBalanceBefore = await rpc.getCompressedTokenAccountsByOwner(charlie.publicKey, { diff --git a/js/compressed-token/tests/e2e/custom-program-id.test.ts b/js/compressed-token/tests/e2e/custom-program-id.test.ts index 56a1681a9a..6fa85466cd 100644 --- a/js/compressed-token/tests/e2e/custom-program-id.test.ts +++ b/js/compressed-token/tests/e2e/custom-program-id.test.ts @@ -22,9 +22,7 @@ describe('custom programId', () => { expect(CompressedTokenProgram.deriveTokenPoolPda(solMint)).toEqual( expectedPoolPda, ); - expect(CompressedTokenProgram.program.programId).toEqual( - defaultProgramId, - ); + expect(CompressedTokenProgram.programId).toEqual(defaultProgramId); // Set new program ID CompressedTokenProgram.setProgramId(newProgramId); @@ -34,7 +32,7 @@ describe('custom programId', () => { expect(CompressedTokenProgram.deriveTokenPoolPda(solMint)).not.toEqual( expectedPoolPda, ); - expect(CompressedTokenProgram.program.programId).toEqual(newProgramId); + expect(CompressedTokenProgram.programId).toEqual(newProgramId); // Reset program ID CompressedTokenProgram.setProgramId(defaultProgramId); diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index dd81b59001..e39729b981 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -41,6 +41,7 @@ "@coral-xyz/borsh": "^0.30.1", "@noble/hashes": "1.5.0", "bn.js": "^5.2.1", + "bs58": "^6.0.0", "buffer": "6.0.3", "buffer-layout": "^1.2.2", "camelcase": "^8.0.0", diff --git a/js/stateless.js/src/state/BN254.ts b/js/stateless.js/src/state/BN254.ts index 27c0bc2cf4..78c33dff06 100644 --- a/js/stateless.js/src/state/BN254.ts +++ b/js/stateless.js/src/state/BN254.ts @@ -5,7 +5,7 @@ import { FIELD_SIZE } from '../constants'; import { PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; -import { bs58 } from '@coral-xyz/anchor/dist/esm/utils/bytes'; +import bs58 from 'bs58'; import { Buffer } from 'buffer'; /** diff --git a/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts b/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts index 0bebcb04a9..bf4aefd5cb 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts @@ -2,7 +2,7 @@ import { ParsedMessageAccount, ParsedTransactionWithMeta, } from '@solana/web3.js'; -import { bs58 } from '@coral-xyz/anchor/dist/cjs/utils/bytes'; +import bs58 from 'bs58'; import { defaultStaticAccountsStruct } from '../../constants'; import { LightSystemProgram } from '../../programs'; import { Rpc } from '../../rpc';