diff --git a/modules/sdk-coin-sol/src/lib/confidentialTransferBuilder.ts b/modules/sdk-coin-sol/src/lib/confidentialTransferBuilder.ts index a62d061ed4..bebcf9cad8 100644 --- a/modules/sdk-coin-sol/src/lib/confidentialTransferBuilder.ts +++ b/modules/sdk-coin-sol/src/lib/confidentialTransferBuilder.ts @@ -159,6 +159,7 @@ export class ConfidentialTransferBuilder extends TransactionBuilder { payerAddress: params.payerAddress, ownerAddress: params.ownerAddress, programId: params.programId, + mintHasTransferFeeConfig: params.mintHasTransferFeeConfig, }, }); return this; @@ -166,14 +167,13 @@ export class ConfidentialTransferBuilder extends TransactionBuilder { /** * Add a ConfigureAccount instruction to the transaction. - * One-time ATA setup — registers ElGamal pubkey + AES zero ciphertext. - * Must be accompanied by a VerifyPubkeyValidity proof instruction. + * One-time ATA setup — registers the AES zero ciphertext and pending-balance counter; the + * ElGamal pubkey is taken from the accompanying VerifyPubkeyValidity proof. */ configureAccount(params: ConfigureConfidentialTransferAccount['params']): this { assert(params.tokenAddress, 'Missing tokenAddress param'); assert(params.mintAddress, 'Missing mintAddress param'); assert(params.authorityAddress, 'Missing authorityAddress param'); - assert(params.elgamalPubkey, 'Missing elgamalPubkey param'); assert(params.decryptableZeroBalance, 'Missing decryptableZeroBalance param'); assert(params.maximumPendingBalanceCreditCounter, 'Missing maximumPendingBalanceCreditCounter param'); diff --git a/modules/sdk-coin-sol/src/lib/iface.ts b/modules/sdk-coin-sol/src/lib/iface.ts index 505efd121d..9a9740b079 100644 --- a/modules/sdk-coin-sol/src/lib/iface.ts +++ b/modules/sdk-coin-sol/src/lib/iface.ts @@ -285,6 +285,11 @@ export interface AtaInit { payerAddress: string; tokenName: string; programId?: string; + /** + * True when the mint carries `TransferFeeConfig`. The ATA then also needs room for the + * ConfidentialTransferFeeAmount extension, so the bundled Reallocate reserves it. + */ + mintHasTransferFeeConfig?: boolean; }; } @@ -296,6 +301,11 @@ export interface ReallocateConfidentialTransferAccount { payerAddress: string; ownerAddress: string; programId?: string; + /** + * True when the mint carries `TransferFeeConfig`. ConfigureAccount initializes + * ConfidentialTransferFeeAmount for such mints, so its 68 bytes must be reserved here. + */ + mintHasTransferFeeConfig?: boolean; }; } @@ -413,6 +423,9 @@ export interface TransactionExplanation extends BaseTransactionExplanation { * Layout: [27][2] + decryptable_zero_balance(36) + maximum_pending_balance_credit_counter(u64) + proof_instruction_offset(i8) = 47 bytes * * Accounts: [token(writable), mint, instructionsSysvar|contextState, authority(signer)] + * + * The account's ElGamal pubkey is not part of the instruction data — Token-2022 takes it + * from the accompanying VerifyPubkeyValidity proof context. */ export interface ConfigureConfidentialTransferAccount { type: InstructionBuilderTypes.ConfigureConfidentialTransferAccount; @@ -420,8 +433,6 @@ export interface ConfigureConfidentialTransferAccount { tokenAddress: string; mintAddress: string; authorityAddress: string; - /** 32-byte ElGamal pubkey registered on the token account (hex). */ - elgamalPubkey: string; /** Instructions sysvar (inline proof) or context state account (pre-verified proof). Defaults to instructions sysvar. */ instructionsSysvarOrContextStateAddress?: string; /** AES-encrypted zero balance (36 bytes hex). */ diff --git a/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts b/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts index eb60496850..de76c5c478 100644 --- a/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts +++ b/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts @@ -606,6 +606,12 @@ function createATAInstruction(data: AtaInit): TransactionInstruction[] { return [associatedTokenAccountInstruction]; } +// Token-2022 `ExtensionType::ConfidentialTransferFeeAmount`. The pinned `@solana/spl-token` +// enum predates the confidential-fee members (it jumps TransferHookAccount = 15 to +// MetadataPointer = 18), so the value is restated from the Token-2022 enum order: +// ConfidentialTransferAccount = 5, ConfidentialTransferFeeAmount = 17. +const CONFIDENTIAL_TRANSFER_FEE_AMOUNT_EXTENSION_TYPE = 17; + /** * Construct a Token-2022 Reallocate instruction that adds the ConfidentialTransferAccount * extension. ConfigureAccount fails on-chain unless the account already holds the extension, @@ -618,18 +624,26 @@ function reallocateConfidentialTransferAccountInstruction( data: ReallocateConfidentialTransferAccount ): TransactionInstruction[] { const { - params: { tokenAddress, payerAddress, ownerAddress, programId }, + params: { tokenAddress, payerAddress, ownerAddress, programId, mintHasTransferFeeConfig }, } = data; assert(tokenAddress, 'Missing tokenAddress param'); assert(payerAddress, 'Missing payerAddress param'); assert(ownerAddress, 'Missing ownerAddress param'); assert(programId && programId === TOKEN_2022_PROGRAM_ID.toString(), 'Reallocate requires the Token-2022 program'); + const extensionTypes: ExtensionType[] = [ExtensionType.ConfidentialTransferAccount]; + // ConfigureAccount also initializes ConfidentialTransferFeeAmount when the mint carries + // TransferFeeConfig, so the reallocation must reserve that extension's 68 bytes (64 + TLV + // header) or the configuration fails on-chain for fee-bearing mints. + if (mintHasTransferFeeConfig) { + extensionTypes.push(CONFIDENTIAL_TRANSFER_FEE_AMOUNT_EXTENSION_TYPE as ExtensionType); + } + return [ createReallocateInstruction( new PublicKey(tokenAddress), new PublicKey(payerAddress), - [ExtensionType.ConfidentialTransferAccount], + extensionTypes, new PublicKey(ownerAddress), [], TOKEN_2022_PROGRAM_ID @@ -991,21 +1005,22 @@ function assertValidI8(value: number, fieldName: string): void { } // Buffer layouts for CT instructions with multi-byte fields +// Token-2022 `ConfigureAccountInstructionData` carries no ElGamal pubkey: the key is +// extracted from the accompanying VerifyPubkeyValidity proof context. Layout: +// [27][2] + decryptable_zero_balance(36) + maximum_pending_balance_credit_counter(u64) + proof_instruction_offset(i8). const configureAccountDataLayout = struct<{ instruction: number; confidentialTransferInstruction: number; - elgamalPubkey: Uint8Array; decryptableZeroBalance: Uint8Array; maximumPendingBalanceCreditCounter: Uint8Array; proofInstructionOffset: number; }>([ u8('instruction'), // 27 u8('confidentialTransferInstruction'), // 2 - blob(32, 'elgamalPubkey'), // new_account_elgamal_pubkey blob(36, 'decryptableZeroBalance'), blob(8, 'maximumPendingBalanceCreditCounter'), s8('proofInstructionOffset'), // i8 — signed: negative = proof is before this instruction -]); // span = 79 +]); // span = 47 const applyPendingBalanceDataLayout = struct<{ instruction: number; @@ -1072,7 +1087,6 @@ function configureConfidentialTransferAccountInstruction( tokenAddress, mintAddress, authorityAddress, - elgamalPubkey, instructionsSysvarOrContextStateAddress, decryptableZeroBalance, maximumPendingBalanceCreditCounter, @@ -1082,7 +1096,6 @@ function configureConfidentialTransferAccountInstruction( assert(tokenAddress, 'Missing tokenAddress param'); assert(mintAddress, 'Missing mintAddress param'); assert(authorityAddress, 'Missing authorityAddress param'); - assert(elgamalPubkey, 'Missing elgamalPubkey param'); assert(decryptableZeroBalance, 'Missing decryptableZeroBalance param'); assertValidAmountString(maximumPendingBalanceCreditCounter, 'maximumPendingBalanceCreditCounter'); assertValidI8(proofInstructionOffset, 'proofInstructionOffset'); @@ -1100,8 +1113,6 @@ function configureConfidentialTransferAccountInstruction( const decryptableBytes = Buffer.from(decryptableZeroBalance, 'hex'); assert(decryptableBytes.length === 36, 'decryptableZeroBalance must be 36 bytes'); - const elgamalPubkeyBytes = Buffer.from(elgamalPubkey, 'hex'); - assert(elgamalPubkeyBytes.length === 32, 'elgamalPubkey must be 32 bytes'); const dataBuffer = Buffer.alloc(configureAccountDataLayout.span); const counterBuffer = Buffer.alloc(8); @@ -1111,7 +1122,6 @@ function configureConfidentialTransferAccountInstruction( { instruction: CT_EXT_DISCRIMINATOR, confidentialTransferInstruction: CT_CONFIGURE_ACCOUNT_DISCRIMINATOR, - elgamalPubkey: elgamalPubkeyBytes, decryptableZeroBalance: decryptableBytes, maximumPendingBalanceCreditCounter: counterBuffer, proofInstructionOffset, diff --git a/modules/sdk-coin-sol/test/unit/ctEnablementVerify.ts b/modules/sdk-coin-sol/test/unit/ctEnablementVerify.ts index e73724a1e7..ec06a65dbc 100644 --- a/modules/sdk-coin-sol/test/unit/ctEnablementVerify.ts +++ b/modules/sdk-coin-sol/test/unit/ctEnablementVerify.ts @@ -56,7 +56,6 @@ describe('SOL: CT enablement token verification', function () { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '18446744073709551615', proofInstructionOffset: 1, diff --git a/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialEnablementV1.ts b/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialEnablementV1.ts index 9f894c1d57..c58a363b27 100644 --- a/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialEnablementV1.ts +++ b/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialEnablementV1.ts @@ -46,7 +46,6 @@ describe('Solana Confidential Transfer Builder - CT account enablement (v1)', () tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '18446744073709551615', proofInstructionOffset: 1, diff --git a/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialTransferBuilder.ts b/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialTransferBuilder.ts index 8986468685..9e82b9ec14 100644 --- a/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialTransferBuilder.ts +++ b/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialTransferBuilder.ts @@ -35,13 +35,12 @@ describe('Solana Confidential Transfer Builder', () => { const solTx = (tx: any): any => (tx as Transaction).solTransaction; describe('ConfigureAccount', () => { - it('should build a configure account instruction with correct 79-byte layout', async () => { + it('should build a configure account instruction with the 47-byte Token-2022 layout', async () => { const builder = ctBuilder(); builder.configureAccount({ tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '65536', proofInstructionOffset: 1, @@ -56,13 +55,15 @@ describe('Solana Confidential Transfer Builder', () => { const rawInstructions = solTx(tx).instructions; rawInstructions.should.have.length(1); const data = rawInstructions[0].data; - data.length.should.equal(79); + // Token-2022 ConfigureAccount carries no ElGamal pubkey: the key travels in the + // VerifyPubkeyValidity proof instruction's context. Layout is + // [27][2] + decryptable_zero_balance(36) + maximum_pending_balance_credit_counter(u64) + proof_instruction_offset(i8). + data.length.should.equal(47); data[0].should.equal(27); // ConfidentialTransferExtension data[1].should.equal(2); // ConfigureAccount - data.slice(2, 34).toString('hex').should.equal('11'.repeat(32)); // new_account_elgamal_pubkey - data.slice(34, 70).toString('hex').should.equal(zeroBytes36); // decryptable_zero_balance - data.readBigUInt64LE(70).toString().should.equal('65536'); // max pending balance credit counter - data[78].should.equal(1); // proof_instruction_offset + data.slice(2, 38).toString('hex').should.equal(zeroBytes36); // decryptable_zero_balance + data.readBigUInt64LE(38).toString().should.equal('65536'); // max pending balance credit counter + data[46].should.equal(1); // proof_instruction_offset // Verify accounts const keys = rawInstructions[0].keys; @@ -83,7 +84,6 @@ describe('Solana Confidential Transfer Builder', () => { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), instructionsSysvarOrContextStateAddress: contextStateAddress, decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '65536', @@ -94,7 +94,55 @@ describe('Solana Confidential Transfer Builder', () => { const keys = solTx(tx).instructions[0].keys; keys[2].pubkey.toString().should.equal(contextStateAddress); const data = solTx(tx).instructions[0].data; - data[78].should.equal(0); // proof_instruction_offset = 0 (context state) + data[46].should.equal(0); // proof_instruction_offset = 0 (context state) + }); + }); + + describe('createAta reallocate extension list', () => { + const token2022ProgramId = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'; + // Reallocate data: [29][extensionType(u16 LE), ...]. ExtensionType values follow the + // Token-2022 enum: ConfidentialTransferAccount = 5, ConfidentialTransferFeeAmount = 17. + const REALLOCATE = 29; + const CONFIDENTIAL_TRANSFER_ACCOUNT = 5; + const CONFIDENTIAL_TRANSFER_FEE_AMOUNT = 17; + + it('should reallocate only ConfidentialTransferAccount for a mint without TransferFeeConfig', async () => { + const builder = ctBuilder(); + builder.createAta({ + mintAddress, + ataAddress: tokenAddress, + ownerAddress: authorityAddress, + payerAddress: account.pub, + tokenName: 'tsol:ray', + programId: token2022ProgramId, + }); + + const tx = await builder.build(); + solTx(tx).instructions.should.have.length(2); + const data = solTx(tx).instructions[1].data; + data.length.should.equal(3); + data[0].should.equal(REALLOCATE); + data.readUInt16LE(1).should.equal(CONFIDENTIAL_TRANSFER_ACCOUNT); + }); + + it('should also reserve ConfidentialTransferFeeAmount when the mint has TransferFeeConfig', async () => { + const builder = ctBuilder(); + builder.createAta({ + mintAddress, + ataAddress: tokenAddress, + ownerAddress: authorityAddress, + payerAddress: account.pub, + tokenName: 'tsol:usdc', + programId: token2022ProgramId, + mintHasTransferFeeConfig: true, + }); + + const tx = await builder.build(); + const data = solTx(tx).instructions[1].data; + data.length.should.equal(5); + data[0].should.equal(REALLOCATE); + data.readUInt16LE(1).should.equal(CONFIDENTIAL_TRANSFER_ACCOUNT); + data.readUInt16LE(3).should.equal(CONFIDENTIAL_TRANSFER_FEE_AMOUNT); }); }); @@ -457,7 +505,6 @@ describe('Solana Confidential Transfer Builder', () => { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '65536', proofInstructionOffset: 1, @@ -482,7 +529,6 @@ describe('Solana Confidential Transfer Builder', () => { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '65536', proofInstructionOffset: 1, @@ -507,7 +553,6 @@ describe('Solana Confidential Transfer Builder', () => { tokenAddress: '', mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '65536', proofInstructionOffset: 1, @@ -525,7 +570,6 @@ describe('Solana Confidential Transfer Builder', () => { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '65536', proofInstructionOffset: -1, // proof is the previous instruction @@ -533,7 +577,7 @@ describe('Solana Confidential Transfer Builder', () => { const tx = await builder.build(); const data = solTx(tx).instructions[0].data; - data[78].should.equal(255); // -1 as i8 = 255 as u8 (two's complement) + data[46].should.equal(255); // -1 as i8 = 255 as u8 (two's complement) }); it('should correctly encode negative offset on Withdraw', async () => { @@ -715,7 +759,6 @@ describe('Solana Confidential Transfer Builder', () => { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: 'foo', proofInstructionOffset: 1, @@ -767,7 +810,6 @@ describe('Solana Confidential Transfer Builder', () => { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '65536', proofInstructionOffset: 1, @@ -845,7 +887,6 @@ describe('Solana Confidential Transfer Builder', () => { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '65536', proofInstructionOffset: 128, @@ -859,7 +900,6 @@ describe('Solana Confidential Transfer Builder', () => { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '65536', proofInstructionOffset: -129, @@ -927,7 +967,6 @@ describe('Solana Confidential Transfer Builder', () => { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '65536', proofInstructionOffset: 127, @@ -935,7 +974,7 @@ describe('Solana Confidential Transfer Builder', () => { const tx = await builder.build(); const data = solTx(tx).instructions[0].data; - data[78].should.equal(127); + data[46].should.equal(127); }); }); @@ -973,7 +1012,6 @@ describe('Solana Confidential Transfer Builder', () => { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '18446744073709551616', // 2^64 proofInstructionOffset: 1, diff --git a/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialTransferV1.ts b/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialTransferV1.ts index dbe36989c1..323514696f 100644 --- a/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialTransferV1.ts +++ b/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialTransferV1.ts @@ -169,7 +169,6 @@ describe('Solana Confidential Transfer Builder - v1', () => { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '65536', proofInstructionOffset: 1, diff --git a/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialTransferV1Ops.ts b/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialTransferV1Ops.ts index 59a42d701a..c8368aba06 100644 --- a/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialTransferV1Ops.ts +++ b/modules/sdk-coin-sol/test/unit/transactionBuilder/confidentialTransferV1Ops.ts @@ -124,7 +124,6 @@ describe('Solana Confidential Transfer Builder - v1 per-op', () => { tokenAddress, mintAddress, authorityAddress, - elgamalPubkey: '11'.repeat(32), decryptableZeroBalance: zeroBytes36, maximumPendingBalanceCreditCounter: '65536', proofInstructionOffset: 1,