From 5a0982f3a6e6ee42ab4c74eb139f83b1a2b12eb9 Mon Sep 17 00:00:00 2001 From: catmcgee Date: Tue, 26 May 2026 15:17:42 -0300 Subject: [PATCH 1/2] interface: add auditor ciphertext fields to Transfer data --- .../instructions/confidentialTransfer.ts | 30 +++++++++ .../confidentialTransfer.test.ts | 67 +++++++++++++++++++ interface/idl.json | 24 +++++++ 3 files changed, 121 insertions(+) create mode 100644 clients/js/test/extensions/confidentialTransfer/confidentialTransfer.test.ts diff --git a/clients/js/src/generated/instructions/confidentialTransfer.ts b/clients/js/src/generated/instructions/confidentialTransfer.ts index 3e015861a..c51d98ef4 100644 --- a/clients/js/src/generated/instructions/confidentialTransfer.ts +++ b/clients/js/src/generated/instructions/confidentialTransfer.ts @@ -36,8 +36,12 @@ import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; import { getDecryptableBalanceDecoder, getDecryptableBalanceEncoder, + getEncryptedBalanceDecoder, + getEncryptedBalanceEncoder, type DecryptableBalance, type DecryptableBalanceArgs, + type EncryptedBalance, + type EncryptedBalanceArgs, } from '../types'; export const CONFIDENTIAL_TRANSFER_DISCRIMINATOR = 27; @@ -90,6 +94,16 @@ export type ConfidentialTransferInstructionData = { confidentialTransferDiscriminator: number; /** The new source decryptable balance if the transfer succeeds. */ newSourceDecryptableAvailableBalance: DecryptableBalance; + /** + * The low 16 bits of the transfer amount encrypted under the auditor + * ElGamal public key. + */ + transferAmountAuditorCiphertextLo: EncryptedBalance; + /** + * The high 32 bits of the transfer amount encrypted under the auditor + * ElGamal public key. + */ + transferAmountAuditorCiphertextHi: EncryptedBalance; /** * Relative location of the * `ProofInstruction::VerifyCiphertextCommitmentEquality` instruction @@ -115,6 +129,16 @@ export type ConfidentialTransferInstructionData = { export type ConfidentialTransferInstructionDataArgs = { /** The new source decryptable balance if the transfer succeeds. */ newSourceDecryptableAvailableBalance: DecryptableBalanceArgs; + /** + * The low 16 bits of the transfer amount encrypted under the auditor + * ElGamal public key. + */ + transferAmountAuditorCiphertextLo: EncryptedBalanceArgs; + /** + * The high 32 bits of the transfer amount encrypted under the auditor + * ElGamal public key. + */ + transferAmountAuditorCiphertextHi: EncryptedBalanceArgs; /** * Relative location of the * `ProofInstruction::VerifyCiphertextCommitmentEquality` instruction @@ -143,6 +167,8 @@ export function getConfidentialTransferInstructionDataEncoder(): FixedSizeEncode ['discriminator', getU8Encoder()], ['confidentialTransferDiscriminator', getU8Encoder()], ['newSourceDecryptableAvailableBalance', getDecryptableBalanceEncoder()], + ['transferAmountAuditorCiphertextLo', getEncryptedBalanceEncoder()], + ['transferAmountAuditorCiphertextHi', getEncryptedBalanceEncoder()], ['equalityProofInstructionOffset', getI8Encoder()], ['ciphertextValidityProofInstructionOffset', getI8Encoder()], ['rangeProofInstructionOffset', getI8Encoder()], @@ -160,6 +186,8 @@ export function getConfidentialTransferInstructionDataDecoder(): FixedSizeDecode ['discriminator', getU8Decoder()], ['confidentialTransferDiscriminator', getU8Decoder()], ['newSourceDecryptableAvailableBalance', getDecryptableBalanceDecoder()], + ['transferAmountAuditorCiphertextLo', getEncryptedBalanceDecoder()], + ['transferAmountAuditorCiphertextHi', getEncryptedBalanceDecoder()], ['equalityProofInstructionOffset', getI8Decoder()], ['ciphertextValidityProofInstructionOffset', getI8Decoder()], ['rangeProofInstructionOffset', getI8Decoder()], @@ -207,6 +235,8 @@ export type ConfidentialTransferInput< /** The source account's owner/delegate or its multisignature account. */ authority: Address | TransactionSigner; newSourceDecryptableAvailableBalance: ConfidentialTransferInstructionDataArgs['newSourceDecryptableAvailableBalance']; + transferAmountAuditorCiphertextLo: ConfidentialTransferInstructionDataArgs['transferAmountAuditorCiphertextLo']; + transferAmountAuditorCiphertextHi: ConfidentialTransferInstructionDataArgs['transferAmountAuditorCiphertextHi']; equalityProofInstructionOffset: ConfidentialTransferInstructionDataArgs['equalityProofInstructionOffset']; ciphertextValidityProofInstructionOffset: ConfidentialTransferInstructionDataArgs['ciphertextValidityProofInstructionOffset']; rangeProofInstructionOffset: ConfidentialTransferInstructionDataArgs['rangeProofInstructionOffset']; diff --git a/clients/js/test/extensions/confidentialTransfer/confidentialTransfer.test.ts b/clients/js/test/extensions/confidentialTransfer/confidentialTransfer.test.ts new file mode 100644 index 000000000..ad56225f5 --- /dev/null +++ b/clients/js/test/extensions/confidentialTransfer/confidentialTransfer.test.ts @@ -0,0 +1,67 @@ +import { address, generateKeyPairSigner } from '@solana/kit'; +import test from 'ava'; +import { getConfidentialTransferInstruction, parseConfidentialTransferInstruction } from '../../../src'; + +const SOURCE_AUDITOR_CIPHERTEXT_LO = new Uint8Array(64).fill(0xab); +const SOURCE_AUDITOR_CIPHERTEXT_HI = new Uint8Array(64).fill(0xcd); +const NEW_SOURCE_DECRYPTABLE_BALANCE = new Uint8Array(36).fill(0xef); + +test('it encodes the auditor ciphertext fields into the Transfer instruction data', async t => { + const [sourceToken, mint, destinationToken, authority] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfidentialTransferInstruction({ + sourceToken: sourceToken.address, + mint: mint.address, + destinationToken: destinationToken.address, + instructionsSysvar: address('Sysvar1nstructions1111111111111111111111111'), + authority, + newSourceDecryptableAvailableBalance: NEW_SOURCE_DECRYPTABLE_BALANCE, + transferAmountAuditorCiphertextLo: SOURCE_AUDITOR_CIPHERTEXT_LO, + transferAmountAuditorCiphertextHi: SOURCE_AUDITOR_CIPHERTEXT_HI, + equalityProofInstructionOffset: 1, + ciphertextValidityProofInstructionOffset: 2, + rangeProofInstructionOffset: 3, + }); + + const parsed = parseConfidentialTransferInstruction(instruction); + + t.deepEqual(parsed.data.transferAmountAuditorCiphertextLo, SOURCE_AUDITOR_CIPHERTEXT_LO); + t.deepEqual(parsed.data.transferAmountAuditorCiphertextHi, SOURCE_AUDITOR_CIPHERTEXT_HI); + t.deepEqual(parsed.data.newSourceDecryptableAvailableBalance, NEW_SOURCE_DECRYPTABLE_BALANCE); + t.is(parsed.data.equalityProofInstructionOffset, 1); + t.is(parsed.data.ciphertextValidityProofInstructionOffset, 2); + t.is(parsed.data.rangeProofInstructionOffset, 3); +}); + +test('it encodes the auditor ciphertexts at the documented byte offsets', async t => { + const [sourceToken, mint, destinationToken, authority] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfidentialTransferInstruction({ + sourceToken: sourceToken.address, + mint: mint.address, + destinationToken: destinationToken.address, + instructionsSysvar: address('Sysvar1nstructions1111111111111111111111111'), + authority, + newSourceDecryptableAvailableBalance: NEW_SOURCE_DECRYPTABLE_BALANCE, + transferAmountAuditorCiphertextLo: SOURCE_AUDITOR_CIPHERTEXT_LO, + transferAmountAuditorCiphertextHi: SOURCE_AUDITOR_CIPHERTEXT_HI, + equalityProofInstructionOffset: 0, + ciphertextValidityProofInstructionOffset: 0, + rangeProofInstructionOffset: 0, + }); + + // Data layout: 1 byte outer disc + 1 byte inner disc + 36 bytes AE balance + // = 38 bytes before the lo ciphertext, 102 bytes before the hi ciphertext. + t.deepEqual(instruction.data.slice(38, 102), SOURCE_AUDITOR_CIPHERTEXT_LO); + t.deepEqual(instruction.data.slice(102, 166), SOURCE_AUDITOR_CIPHERTEXT_HI); +}); diff --git a/interface/idl.json b/interface/idl.json index cf4a2a5c9..710934952 100644 --- a/interface/idl.json +++ b/interface/idl.json @@ -4002,6 +4002,30 @@ "name": "decryptableBalance" } }, + { + "kind": "instructionArgumentNode", + "name": "transferAmountAuditorCiphertextLo", + "docs": [ + "The low 16 bits of the transfer amount encrypted under the auditor", + "ElGamal public key." + ], + "type": { + "kind": "definedTypeLinkNode", + "name": "encryptedBalance" + } + }, + { + "kind": "instructionArgumentNode", + "name": "transferAmountAuditorCiphertextHi", + "docs": [ + "The high 32 bits of the transfer amount encrypted under the auditor", + "ElGamal public key." + ], + "type": { + "kind": "definedTypeLinkNode", + "name": "encryptedBalance" + } + }, { "kind": "instructionArgumentNode", "name": "equalityProofInstructionOffset", From 43e5af2bd48baa177e04bc2352f052d9d5e34501 Mon Sep 17 00:00:00 2001 From: catmcgee Date: Tue, 26 May 2026 15:25:34 -0300 Subject: [PATCH 2/2] interface: omit absent optional accounts in Withdraw and Transfer --- .../instructions/confidentialTransfer.ts | 52 +++++--- .../instructions/confidentialWithdraw.ts | 40 +++++-- .../confidentialTransfer.test.ts | 111 +++++++++++++++++- .../confidentialWithdraw.test.ts | 96 +++++++++++++++ interface/idl.json | 4 +- 5 files changed, 268 insertions(+), 35 deletions(-) create mode 100644 clients/js/test/extensions/confidentialTransfer/confidentialWithdraw.test.ts diff --git a/clients/js/src/generated/instructions/confidentialTransfer.ts b/clients/js/src/generated/instructions/confidentialTransfer.ts index c51d98ef4..9aa271e48 100644 --- a/clients/js/src/generated/instructions/confidentialTransfer.ts +++ b/clients/js/src/generated/instructions/confidentialTransfer.ts @@ -61,10 +61,10 @@ export type ConfidentialTransferInstruction< TAccountSourceToken extends string | AccountMeta = string, TAccountMint extends string | AccountMeta = string, TAccountDestinationToken extends string | AccountMeta = string, - TAccountInstructionsSysvar extends string | AccountMeta = string, - TAccountEqualityRecord extends string | AccountMeta = string, - TAccountCiphertextValidityRecord extends string | AccountMeta = string, - TAccountRangeRecord extends string | AccountMeta = string, + TAccountInstructionsSysvar extends string | AccountMeta | undefined = undefined, + TAccountEqualityRecord extends string | AccountMeta | undefined = undefined, + TAccountCiphertextValidityRecord extends string | AccountMeta | undefined = undefined, + TAccountRangeRecord extends string | AccountMeta | undefined = undefined, TAccountAuthority extends string | AccountMeta = string, TRemainingAccounts extends readonly AccountMeta[] = [], > = Instruction & @@ -76,14 +76,30 @@ export type ConfidentialTransferInstruction< TAccountDestinationToken extends string ? WritableAccount : TAccountDestinationToken, - TAccountInstructionsSysvar extends string - ? ReadonlyAccount - : TAccountInstructionsSysvar, - TAccountEqualityRecord extends string ? ReadonlyAccount : TAccountEqualityRecord, - TAccountCiphertextValidityRecord extends string - ? ReadonlyAccount - : TAccountCiphertextValidityRecord, - TAccountRangeRecord extends string ? ReadonlyAccount : TAccountRangeRecord, + ...(TAccountInstructionsSysvar extends undefined + ? [] + : [ + TAccountInstructionsSysvar extends string + ? ReadonlyAccount + : TAccountInstructionsSysvar, + ]), + ...(TAccountEqualityRecord extends undefined + ? [] + : [ + TAccountEqualityRecord extends string + ? ReadonlyAccount + : TAccountEqualityRecord, + ]), + ...(TAccountCiphertextValidityRecord extends undefined + ? [] + : [ + TAccountCiphertextValidityRecord extends string + ? ReadonlyAccount + : TAccountCiphertextValidityRecord, + ]), + ...(TAccountRangeRecord extends undefined + ? [] + : [TAccountRangeRecord extends string ? ReadonlyAccount : TAccountRangeRecord]), TAccountAuthority extends string ? ReadonlyAccount : TAccountAuthority, ...TRemainingAccounts, ] @@ -304,7 +320,7 @@ export function getConfidentialTransferInstruction< signer, })); - const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + const getAccountMeta = getAccountMetaFactory(programAddress, 'omitted'); return Object.freeze({ accounts: [ getAccountMeta(accounts.sourceToken), @@ -316,7 +332,7 @@ export function getConfidentialTransferInstruction< getAccountMeta(accounts.rangeRecord), getAccountMeta(accounts.authority), ...remainingAccounts, - ], + ].filter((x: T | undefined): x is T => x !== undefined), data: getConfidentialTransferInstructionDataEncoder().encode(args as ConfidentialTransferInstructionDataArgs), programAddress, } as ConfidentialTransferInstruction< @@ -372,7 +388,7 @@ export function parseConfidentialTransferInstruction< InstructionWithAccounts & InstructionWithData, ): ParsedConfidentialTransferInstruction { - if (instruction.accounts.length < 8) { + if (instruction.accounts.length < 4) { // TODO: Coded error. throw new Error('Not enough accounts'); } @@ -382,9 +398,11 @@ export function parseConfidentialTransferInstruction< accountIndex += 1; return accountMeta; }; + let optionalAccountsRemaining = instruction.accounts.length - 4; const getNextOptionalAccount = () => { - const accountMeta = getNextAccount(); - return accountMeta.address === TOKEN_2022_PROGRAM_ADDRESS ? undefined : accountMeta; + if (optionalAccountsRemaining === 0) return undefined; + optionalAccountsRemaining -= 1; + return getNextAccount(); }; return { programAddress: instruction.programAddress, diff --git a/clients/js/src/generated/instructions/confidentialWithdraw.ts b/clients/js/src/generated/instructions/confidentialWithdraw.ts index da3f7861f..fa31520c5 100644 --- a/clients/js/src/generated/instructions/confidentialWithdraw.ts +++ b/clients/js/src/generated/instructions/confidentialWithdraw.ts @@ -58,9 +58,9 @@ export type ConfidentialWithdrawInstruction< TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, TAccountToken extends string | AccountMeta = string, TAccountMint extends string | AccountMeta = string, - TAccountInstructionsSysvar extends string | AccountMeta = string, - TAccountEqualityRecord extends string | AccountMeta = string, - TAccountRangeRecord extends string | AccountMeta = string, + TAccountInstructionsSysvar extends string | AccountMeta | undefined = undefined, + TAccountEqualityRecord extends string | AccountMeta | undefined = undefined, + TAccountRangeRecord extends string | AccountMeta | undefined = undefined, TAccountAuthority extends string | AccountMeta = string, TRemainingAccounts extends readonly AccountMeta[] = [], > = Instruction & @@ -69,11 +69,23 @@ export type ConfidentialWithdrawInstruction< [ TAccountToken extends string ? WritableAccount : TAccountToken, TAccountMint extends string ? ReadonlyAccount : TAccountMint, - TAccountInstructionsSysvar extends string - ? ReadonlyAccount - : TAccountInstructionsSysvar, - TAccountEqualityRecord extends string ? ReadonlyAccount : TAccountEqualityRecord, - TAccountRangeRecord extends string ? ReadonlyAccount : TAccountRangeRecord, + ...(TAccountInstructionsSysvar extends undefined + ? [] + : [ + TAccountInstructionsSysvar extends string + ? ReadonlyAccount + : TAccountInstructionsSysvar, + ]), + ...(TAccountEqualityRecord extends undefined + ? [] + : [ + TAccountEqualityRecord extends string + ? ReadonlyAccount + : TAccountEqualityRecord, + ]), + ...(TAccountRangeRecord extends undefined + ? [] + : [TAccountRangeRecord extends string ? ReadonlyAccount : TAccountRangeRecord]), TAccountAuthority extends string ? ReadonlyAccount : TAccountAuthority, ...TRemainingAccounts, ] @@ -251,7 +263,7 @@ export function getConfidentialWithdrawInstruction< signer, })); - const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + const getAccountMeta = getAccountMetaFactory(programAddress, 'omitted'); return Object.freeze({ accounts: [ getAccountMeta(accounts.token), @@ -261,7 +273,7 @@ export function getConfidentialWithdrawInstruction< getAccountMeta(accounts.rangeRecord), getAccountMeta(accounts.authority), ...remainingAccounts, - ], + ].filter((x: T | undefined): x is T => x !== undefined), data: getConfidentialWithdrawInstructionDataEncoder().encode(args as ConfidentialWithdrawInstructionDataArgs), programAddress, } as ConfidentialWithdrawInstruction< @@ -311,7 +323,7 @@ export function parseConfidentialWithdrawInstruction< InstructionWithAccounts & InstructionWithData, ): ParsedConfidentialWithdrawInstruction { - if (instruction.accounts.length < 6) { + if (instruction.accounts.length < 3) { // TODO: Coded error. throw new Error('Not enough accounts'); } @@ -321,9 +333,11 @@ export function parseConfidentialWithdrawInstruction< accountIndex += 1; return accountMeta; }; + let optionalAccountsRemaining = instruction.accounts.length - 3; const getNextOptionalAccount = () => { - const accountMeta = getNextAccount(); - return accountMeta.address === TOKEN_2022_PROGRAM_ADDRESS ? undefined : accountMeta; + if (optionalAccountsRemaining === 0) return undefined; + optionalAccountsRemaining -= 1; + return getNextAccount(); }; return { programAddress: instruction.programAddress, diff --git a/clients/js/test/extensions/confidentialTransfer/confidentialTransfer.test.ts b/clients/js/test/extensions/confidentialTransfer/confidentialTransfer.test.ts index ad56225f5..4a84fe8f6 100644 --- a/clients/js/test/extensions/confidentialTransfer/confidentialTransfer.test.ts +++ b/clients/js/test/extensions/confidentialTransfer/confidentialTransfer.test.ts @@ -1,7 +1,8 @@ -import { address, generateKeyPairSigner } from '@solana/kit'; +import { address, generateKeyPairSigner, type AccountMeta } from '@solana/kit'; import test from 'ava'; import { getConfidentialTransferInstruction, parseConfidentialTransferInstruction } from '../../../src'; +const SYSVAR_INSTRUCTIONS_ADDRESS = address('Sysvar1nstructions1111111111111111111111111'); const SOURCE_AUDITOR_CIPHERTEXT_LO = new Uint8Array(64).fill(0xab); const SOURCE_AUDITOR_CIPHERTEXT_HI = new Uint8Array(64).fill(0xcd); const NEW_SOURCE_DECRYPTABLE_BALANCE = new Uint8Array(36).fill(0xef); @@ -18,7 +19,7 @@ test('it encodes the auditor ciphertext fields into the Transfer instruction dat sourceToken: sourceToken.address, mint: mint.address, destinationToken: destinationToken.address, - instructionsSysvar: address('Sysvar1nstructions1111111111111111111111111'), + instructionsSysvar: SYSVAR_INSTRUCTIONS_ADDRESS, authority, newSourceDecryptableAvailableBalance: NEW_SOURCE_DECRYPTABLE_BALANCE, transferAmountAuditorCiphertextLo: SOURCE_AUDITOR_CIPHERTEXT_LO, @@ -50,7 +51,7 @@ test('it encodes the auditor ciphertexts at the documented byte offsets', async sourceToken: sourceToken.address, mint: mint.address, destinationToken: destinationToken.address, - instructionsSysvar: address('Sysvar1nstructions1111111111111111111111111'), + instructionsSysvar: SYSVAR_INSTRUCTIONS_ADDRESS, authority, newSourceDecryptableAvailableBalance: NEW_SOURCE_DECRYPTABLE_BALANCE, transferAmountAuditorCiphertextLo: SOURCE_AUDITOR_CIPHERTEXT_LO, @@ -65,3 +66,107 @@ test('it encodes the auditor ciphertexts at the documented byte offsets', async t.deepEqual(instruction.data.slice(38, 102), SOURCE_AUDITOR_CIPHERTEXT_LO); t.deepEqual(instruction.data.slice(102, 166), SOURCE_AUDITOR_CIPHERTEXT_HI); }); + +test('it skips all optional accounts when every proof is in context-state mode', async t => { + const [sourceToken, mint, destinationToken, equalityRecord, ciphertextValidityRecord, rangeRecord, authority] = + await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfidentialTransferInstruction({ + sourceToken: sourceToken.address, + mint: mint.address, + destinationToken: destinationToken.address, + equalityRecord: equalityRecord.address, + ciphertextValidityRecord: ciphertextValidityRecord.address, + rangeRecord: rangeRecord.address, + authority, + newSourceDecryptableAvailableBalance: NEW_SOURCE_DECRYPTABLE_BALANCE, + transferAmountAuditorCiphertextLo: SOURCE_AUDITOR_CIPHERTEXT_LO, + transferAmountAuditorCiphertextHi: SOURCE_AUDITOR_CIPHERTEXT_HI, + equalityProofInstructionOffset: 0, + ciphertextValidityProofInstructionOffset: 0, + rangeProofInstructionOffset: 0, + }); + const accounts = instruction.accounts as readonly AccountMeta[]; + + t.is(accounts.length, 7); + t.is(accounts[0].address, sourceToken.address); + t.is(accounts[1].address, mint.address); + t.is(accounts[2].address, destinationToken.address); + t.is(accounts[3].address, equalityRecord.address); + t.is(accounts[4].address, ciphertextValidityRecord.address); + t.is(accounts[5].address, rangeRecord.address); + t.is(accounts[6].address, authority.address); +}); + +test('it emits only the sysvar when every proof is inline', async t => { + const [sourceToken, mint, destinationToken, authority] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfidentialTransferInstruction({ + sourceToken: sourceToken.address, + mint: mint.address, + destinationToken: destinationToken.address, + instructionsSysvar: SYSVAR_INSTRUCTIONS_ADDRESS, + authority, + newSourceDecryptableAvailableBalance: NEW_SOURCE_DECRYPTABLE_BALANCE, + transferAmountAuditorCiphertextLo: SOURCE_AUDITOR_CIPHERTEXT_LO, + transferAmountAuditorCiphertextHi: SOURCE_AUDITOR_CIPHERTEXT_HI, + equalityProofInstructionOffset: 1, + ciphertextValidityProofInstructionOffset: 2, + rangeProofInstructionOffset: 3, + }); + const accounts = instruction.accounts as readonly AccountMeta[]; + + t.is(accounts.length, 5); + t.is(accounts[0].address, sourceToken.address); + t.is(accounts[1].address, mint.address); + t.is(accounts[2].address, destinationToken.address); + t.is(accounts[3].address, SYSVAR_INSTRUCTIONS_ADDRESS); + t.is(accounts[4].address, authority.address); +}); + +test('it emits sysvar plus only the context-state records that are provided in mixed mode', async t => { + const [sourceToken, mint, destinationToken, equalityRecord, authority] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfidentialTransferInstruction({ + sourceToken: sourceToken.address, + mint: mint.address, + destinationToken: destinationToken.address, + instructionsSysvar: SYSVAR_INSTRUCTIONS_ADDRESS, + equalityRecord: equalityRecord.address, + authority, + newSourceDecryptableAvailableBalance: NEW_SOURCE_DECRYPTABLE_BALANCE, + transferAmountAuditorCiphertextLo: SOURCE_AUDITOR_CIPHERTEXT_LO, + transferAmountAuditorCiphertextHi: SOURCE_AUDITOR_CIPHERTEXT_HI, + equalityProofInstructionOffset: 0, + ciphertextValidityProofInstructionOffset: 1, + rangeProofInstructionOffset: 2, + }); + const accounts = instruction.accounts as readonly AccountMeta[]; + + t.is(accounts.length, 6); + t.is(accounts[0].address, sourceToken.address); + t.is(accounts[1].address, mint.address); + t.is(accounts[2].address, destinationToken.address); + t.is(accounts[3].address, SYSVAR_INSTRUCTIONS_ADDRESS); + t.is(accounts[4].address, equalityRecord.address); + t.is(accounts[5].address, authority.address); +}); diff --git a/clients/js/test/extensions/confidentialTransfer/confidentialWithdraw.test.ts b/clients/js/test/extensions/confidentialTransfer/confidentialWithdraw.test.ts new file mode 100644 index 000000000..1f7c0eab7 --- /dev/null +++ b/clients/js/test/extensions/confidentialTransfer/confidentialWithdraw.test.ts @@ -0,0 +1,96 @@ +import { address, generateKeyPairSigner, type AccountMeta } from '@solana/kit'; +import test from 'ava'; +import { getConfidentialWithdrawInstruction } from '../../../src'; + +const SYSVAR_INSTRUCTIONS_ADDRESS = address('Sysvar1nstructions1111111111111111111111111'); +const NEW_DECRYPTABLE_BALANCE = new Uint8Array(36); + +test('it skips all optional accounts when both proofs are in context-state mode', async t => { + const [token, mint, equalityRecord, rangeRecord, authority] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfidentialWithdrawInstruction({ + token: token.address, + mint: mint.address, + equalityRecord: equalityRecord.address, + rangeRecord: rangeRecord.address, + authority, + amount: 1n, + decimals: 0, + newDecryptableAvailableBalance: NEW_DECRYPTABLE_BALANCE, + equalityProofInstructionOffset: 0, + rangeProofInstructionOffset: 0, + }); + const accounts = instruction.accounts as readonly AccountMeta[]; + + // Layout when both proofs are pre-verified into context state: token, mint, + // equalityRecord, rangeRecord, authority. No sysvar in the accounts list. + t.is(accounts.length, 5); + t.is(accounts[0].address, token.address); + t.is(accounts[1].address, mint.address); + t.is(accounts[2].address, equalityRecord.address); + t.is(accounts[3].address, rangeRecord.address); + t.is(accounts[4].address, authority.address); +}); + +test('it emits only the sysvar when both proofs are inline', async t => { + const [token, mint, authority] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfidentialWithdrawInstruction({ + token: token.address, + mint: mint.address, + instructionsSysvar: SYSVAR_INSTRUCTIONS_ADDRESS, + authority, + amount: 1n, + decimals: 0, + newDecryptableAvailableBalance: NEW_DECRYPTABLE_BALANCE, + equalityProofInstructionOffset: 1, + rangeProofInstructionOffset: 2, + }); + const accounts = instruction.accounts as readonly AccountMeta[]; + + t.is(accounts.length, 4); + t.is(accounts[0].address, token.address); + t.is(accounts[1].address, mint.address); + t.is(accounts[2].address, SYSVAR_INSTRUCTIONS_ADDRESS); + t.is(accounts[3].address, authority.address); +}); + +test('it emits sysvar plus a single context-state account in mixed mode', async t => { + const [token, mint, equalityRecord, authority] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfidentialWithdrawInstruction({ + token: token.address, + mint: mint.address, + instructionsSysvar: SYSVAR_INSTRUCTIONS_ADDRESS, + equalityRecord: equalityRecord.address, + authority, + amount: 1n, + decimals: 0, + newDecryptableAvailableBalance: NEW_DECRYPTABLE_BALANCE, + equalityProofInstructionOffset: 0, + rangeProofInstructionOffset: 1, + }); + const accounts = instruction.accounts as readonly AccountMeta[]; + + t.is(accounts.length, 5); + t.is(accounts[0].address, token.address); + t.is(accounts[1].address, mint.address); + t.is(accounts[2].address, SYSVAR_INSTRUCTIONS_ADDRESS); + t.is(accounts[3].address, equalityRecord.address); + t.is(accounts[4].address, authority.address); +}); diff --git a/interface/idl.json b/interface/idl.json index 710934952..ae4fa3db9 100644 --- a/interface/idl.json +++ b/interface/idl.json @@ -3703,7 +3703,7 @@ "Fails if the source or destination accounts are frozen.", "Fails if the associated mint is extended as `NonTransferable`." ], - "optionalAccountStrategy": "programId", + "optionalAccountStrategy": "omitted", "accounts": [ { "kind": "instructionAccountNode", @@ -3891,7 +3891,7 @@ "", "Fails if the associated mint is extended as `NonTransferable`." ], - "optionalAccountStrategy": "programId", + "optionalAccountStrategy": "omitted", "accounts": [ { "kind": "instructionAccountNode",