Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions modules/sdk-coin-sol/src/lib/confidentialTransferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,21 +159,21 @@ export class ConfidentialTransferBuilder extends TransactionBuilder {
payerAddress: params.payerAddress,
ownerAddress: params.ownerAddress,
programId: params.programId,
mintHasTransferFeeConfig: params.mintHasTransferFeeConfig,
},
});
return this;
}

/**
* 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');

Expand Down
15 changes: 13 additions & 2 deletions modules/sdk-coin-sol/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}

Expand All @@ -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;
};
}

Expand Down Expand Up @@ -413,15 +423,16 @@ 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;
params: {
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). */
Expand Down
30 changes: 20 additions & 10 deletions modules/sdk-coin-sol/src/lib/solInstructionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1072,7 +1087,6 @@ function configureConfidentialTransferAccountInstruction(
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey,
instructionsSysvarOrContextStateAddress,
decryptableZeroBalance,
maximumPendingBalanceCreditCounter,
Expand All @@ -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');
Expand All @@ -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);
Expand All @@ -1111,7 +1122,6 @@ function configureConfidentialTransferAccountInstruction(
{
instruction: CT_EXT_DISCRIMINATOR,
confidentialTransferInstruction: CT_CONFIGURE_ACCOUNT_DISCRIMINATOR,
elgamalPubkey: elgamalPubkeyBytes,
decryptableZeroBalance: decryptableBytes,
maximumPendingBalanceCreditCounter: counterBuffer,
proofInstructionOffset,
Expand Down
1 change: 0 additions & 1 deletion modules/sdk-coin-sol/test/unit/ctEnablementVerify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ describe('SOL: CT enablement token verification', function () {
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: '18446744073709551615',
proofInstructionOffset: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -83,7 +84,6 @@ describe('Solana Confidential Transfer Builder', () => {
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
instructionsSysvarOrContextStateAddress: contextStateAddress,
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: '65536',
Expand All @@ -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);
});
});

Expand Down Expand Up @@ -457,7 +505,6 @@ describe('Solana Confidential Transfer Builder', () => {
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: '65536',
proofInstructionOffset: 1,
Expand All @@ -482,7 +529,6 @@ describe('Solana Confidential Transfer Builder', () => {
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: '65536',
proofInstructionOffset: 1,
Expand All @@ -507,7 +553,6 @@ describe('Solana Confidential Transfer Builder', () => {
tokenAddress: '',
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: '65536',
proofInstructionOffset: 1,
Expand All @@ -525,15 +570,14 @@ describe('Solana Confidential Transfer Builder', () => {
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: '65536',
proofInstructionOffset: -1, // proof is the previous instruction
});

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 () => {
Expand Down Expand Up @@ -715,7 +759,6 @@ describe('Solana Confidential Transfer Builder', () => {
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: 'foo',
proofInstructionOffset: 1,
Expand Down Expand Up @@ -767,7 +810,6 @@ describe('Solana Confidential Transfer Builder', () => {
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: '65536',
proofInstructionOffset: 1,
Expand Down Expand Up @@ -845,7 +887,6 @@ describe('Solana Confidential Transfer Builder', () => {
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: '65536',
proofInstructionOffset: 128,
Expand All @@ -859,7 +900,6 @@ describe('Solana Confidential Transfer Builder', () => {
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: '65536',
proofInstructionOffset: -129,
Expand Down Expand Up @@ -927,15 +967,14 @@ describe('Solana Confidential Transfer Builder', () => {
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: '65536',
proofInstructionOffset: 127,
});

const tx = await builder.build();
const data = solTx(tx).instructions[0].data;
data[78].should.equal(127);
data[46].should.equal(127);
});
});

Expand Down Expand Up @@ -973,7 +1012,6 @@ describe('Solana Confidential Transfer Builder', () => {
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: '18446744073709551616', // 2^64
proofInstructionOffset: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ describe('Solana Confidential Transfer Builder - v1', () => {
tokenAddress,
mintAddress,
authorityAddress,
elgamalPubkey: '11'.repeat(32),
decryptableZeroBalance: zeroBytes36,
maximumPendingBalanceCreditCounter: '65536',
proofInstructionOffset: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading