diff --git a/clients/js/package.json b/clients/js/package.json index acdaee24f..68bea0eb1 100644 --- a/clients/js/package.json +++ b/clients/js/package.json @@ -44,13 +44,19 @@ "homepage": "https://github.com/solana-program/token-2022#readme", "peerDependencies": { "@solana/kit": "^6.0.0", - "@solana/sysvars": "^5.0" + "@solana/sysvars": "^5.0", + "@solana/zk-sdk": "^0.4.2" + }, + "dependencies": { + "@noble/curves": "^1.9.7", + "@solana-program/system": "^0.12.0", + "@solana-program/zk-elgamal-proof": "^0.1.0" }, "devDependencies": { "@ava/typescript": "^7.0.0", - "@solana-program/system": "^0.12.0", "@solana/eslint-config-solana": "^3.0.3", "@solana/kit": "^6.0.0", + "@solana/zk-sdk": "^0.4.2", "@types/node": "^25", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.16.1", diff --git a/clients/js/pnpm-lock.yaml b/clients/js/pnpm-lock.yaml index 66060a847..50cfbab23 100644 --- a/clients/js/pnpm-lock.yaml +++ b/clients/js/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: dependencies: + '@noble/curves': + specifier: ^1.9.7 + version: 1.9.7 + '@solana-program/system': + specifier: ^0.12.0 + version: 0.12.0(@solana/kit@6.9.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) + '@solana-program/zk-elgamal-proof': + specifier: ^0.1.0 + version: 0.1.0(@solana/kit@6.9.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) '@solana/sysvars': specifier: ^5.0 version: 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -15,18 +24,18 @@ importers: '@ava/typescript': specifier: ^7.0.0 version: 7.0.0 - '@solana-program/system': - specifier: ^0.12.0 - version: 0.12.0(@solana/kit@6.9.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) '@solana/eslint-config-solana': specifier: ^3.0.3 version: 3.0.6(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-simple-import-sort@12.1.1(eslint@8.57.1))(eslint-plugin-sort-keys-fix@1.1.2)(eslint-plugin-typescript-sort-keys@3.3.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) '@solana/kit': specifier: ^6.0.0 version: 6.9.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/zk-sdk': + specifier: ^0.4.2 + version: 0.4.2 '@types/node': specifier: ^25 - version: 25.8.0 + version: 25.6.2 '@typescript-eslint/eslint-plugin': specifier: ^7.16.1 version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) @@ -283,6 +292,14 @@ packages: engines: {node: '>=18'} hasBin: true + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -445,6 +462,11 @@ packages: peerDependencies: '@solana/kit': ^6.1.0 + '@solana-program/zk-elgamal-proof@0.1.0': + resolution: {integrity: sha512-MKyeapz8P7SqkkC7h53ARFLoanb59ylVzokSMyINDH7hxY3JyiZs92/VfPa/qedhLAEHLd8jcc8sQ3pr3GyXtw==} + peerDependencies: + '@solana/kit': ^5.0 + '@solana/accounts@5.0.0': resolution: {integrity: sha512-0JzBdEobgp8NBdhhu+GgwNDh7e8KkHDsSTVZAnNQgvT3taOz0Mwv5E48MuEeDhW6DLFwWVAx/FO3pvibG/NGwA==} engines: {node: '>=20.18.0'} @@ -932,6 +954,9 @@ packages: typescript: optional: true + '@solana/zk-sdk@0.4.2': + resolution: {integrity: sha512-lTc3lvg2b14oP7XURa6jmI2m7CLyJR5otf6TVdzw+xo/A9aYvXEQxCqaK7qpfu2JYuzNxRGqP3RnjAvdOSOEUQ==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -941,8 +966,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@25.8.0': - resolution: {integrity: sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==} + '@types/node@25.6.2': + resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==} '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} @@ -2045,11 +2070,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.8.0: - resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} - engines: {node: '>=10'} - hasBin: true - serialize-error@7.0.1: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} @@ -2241,11 +2261,11 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} - undici-types@7.24.6: - resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} - undici-types@8.3.0: - resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + undici-types@8.2.0: + resolution: {integrity: sha512-uciYZ5yCmf+QJb18kJw10HjquzM7K0z992vWcI+84KeBpTfXT4hfgfGJ5DQbf/mCBPACofkrjvqgcjZfuujjFA==} unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} @@ -2510,6 +2530,12 @@ snapshots: - encoding - supports-color + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2627,6 +2653,11 @@ snapshots: dependencies: '@solana/kit': 6.9.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana-program/zk-elgamal-proof@0.1.0(@solana/kit@6.9.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))': + dependencies: + '@solana-program/system': 0.12.0(@solana/kit@6.9.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) + '@solana/kit': 6.9.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/accounts@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -3082,7 +3113,7 @@ snapshots: '@solana/errors': 6.9.0(typescript@5.9.3) '@solana/rpc-spec': 6.9.0(typescript@5.9.3) '@solana/rpc-spec-types': 6.9.0(typescript@5.9.3) - undici-types: 8.3.0 + undici-types: 8.2.0 optionalDependencies: typescript: 5.9.3 @@ -3227,6 +3258,8 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/zk-sdk@0.4.2': {} + '@types/estree@1.0.8': {} '@types/hast@3.0.4': @@ -3235,9 +3268,9 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@25.8.0': + '@types/node@25.6.2': dependencies: - undici-types: 7.24.6 + undici-types: 7.19.2 '@types/semver@7.7.1': {} @@ -3315,7 +3348,7 @@ snapshots: debug: 4.4.3 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.8.0 + semver: 7.7.4 tsutils: 3.21.0(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 @@ -3347,7 +3380,7 @@ snapshots: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3) eslint: 8.57.1 eslint-scope: 5.1.1 - semver: 7.8.0 + semver: 7.7.4 transitivePeerDependencies: - supports-color - typescript @@ -4359,8 +4392,6 @@ snapshots: semver@7.7.4: {} - semver@7.8.0: {} - serialize-error@7.0.1: dependencies: type-fest: 0.13.1 @@ -4546,9 +4577,9 @@ snapshots: ufo@1.6.1: {} - undici-types@7.24.6: {} + undici-types@7.19.2: {} - undici-types@8.3.0: {} + undici-types@8.2.0: {} unicorn-magic@0.3.0: {} diff --git a/clients/js/src/confidentialTransferArithmetic.ts b/clients/js/src/confidentialTransferArithmetic.ts new file mode 100644 index 000000000..3234e89a3 --- /dev/null +++ b/clients/js/src/confidentialTransferArithmetic.ts @@ -0,0 +1,95 @@ +// We depend on @noble/curves because the Web Crypto API does not expose +// Ristretto255 point arithmetic, and `@solana/zk-sdk` (the WASM SDK) does +// not currently expose `ElGamalCiphertext` arithmetic (subtract / multiply +// by scalar / lo-hi combination) over its public API. Once the WASM SDK +// adds those methods, delete this file and call into the SDK directly. +import { ristretto255 } from '@noble/curves/ed25519'; +import { type ReadonlyUint8Array } from '@solana/kit'; + +const { Point: RistrettoPoint } = /* @__PURE__ */ ristretto255; + +function pointFromBytes(bytes: ReadonlyUint8Array) { + return RistrettoPoint.fromHex(new Uint8Array(bytes)); +} + +function ciphertextToPoints(ciphertext: ReadonlyUint8Array) { + if (ciphertext.length !== 64) { + throw new Error(`Expected 64 ciphertext bytes, got ${ciphertext.length}.`); + } + + return { + commitment: pointFromBytes(ciphertext.slice(0, 32)), + handle: pointFromBytes(ciphertext.slice(32, 64)), + }; +} + +function pointsToCiphertext(commitment: ReturnType, handle: ReturnType) { + const ciphertext = new Uint8Array(64); + ciphertext.set(commitment.toRawBytes(), 0); + ciphertext.set(handle.toRawBytes(), 32); + return ciphertext; +} + +/** + * Extracts a single ElGamal ciphertext (commitment + one handle) from a + * grouped ciphertext. The grouped layout is: 32-byte commitment followed + * by N 32-byte handles. The returned 64-byte array is [commitment, handle]. + */ +export function extractCiphertextFromGroupedBytes(groupedCiphertext: ReadonlyUint8Array, handleIndex: number) { + if (!Number.isInteger(handleIndex) || handleIndex < 0) { + throw new Error(`handleIndex must be a non-negative integer, got ${handleIndex}.`); + } + const start = 32 + handleIndex * 32; + const end = start + 32; + if (groupedCiphertext.length < end) { + throw new Error(`Grouped ciphertext does not contain handle ${handleIndex}.`); + } + + const ciphertext = new Uint8Array(64); + ciphertext.set(groupedCiphertext.slice(0, 32), 0); + ciphertext.set(groupedCiphertext.slice(start, end), 32); + return ciphertext; +} + +function subtractCiphertexts(left: ReadonlyUint8Array, right: ReadonlyUint8Array) { + const leftPoints = ciphertextToPoints(left); + const rightPoints = ciphertextToPoints(right); + return pointsToCiphertext( + leftPoints.commitment.subtract(rightPoints.commitment), + leftPoints.handle.subtract(rightPoints.handle), + ); +} + +function combineLoHiCiphertexts(ciphertextLo: ReadonlyUint8Array, ciphertextHi: ReadonlyUint8Array, bitLength: bigint) { + const scale = 1n << bitLength; + const loPoints = ciphertextToPoints(ciphertextLo); + const hiPoints = ciphertextToPoints(ciphertextHi); + return pointsToCiphertext( + loPoints.commitment.add(hiPoints.commitment.multiply(scale)), + loPoints.handle.add(hiPoints.handle.multiply(scale)), + ); +} + +/** + * Combines lo/hi ciphertext halves (hi << bitLength + lo) and subtracts the + * result from `left`. Used to compute the new available-balance ciphertext + * after a confidential transfer. + */ +export function subtractWithLoHiCiphertexts( + left: ReadonlyUint8Array, + ciphertextLo: ReadonlyUint8Array, + ciphertextHi: ReadonlyUint8Array, + bitLength: bigint, +) { + return subtractCiphertexts(left, combineLoHiCiphertexts(ciphertextLo, ciphertextHi, bitLength)); +} + +/** + * Subtracts a plaintext amount from an ElGamal ciphertext by removing + * `amount * G` from the commitment. Used to compute the expected + * remaining-balance ciphertext after a confidential withdraw. + */ +export function subtractAmountFromCiphertext(ciphertext: ReadonlyUint8Array, amount: bigint) { + const { commitment, handle } = ciphertextToPoints(ciphertext); + return pointsToCiphertext(commitment.subtract(RistrettoPoint.BASE.multiply(amount)), handle); +} diff --git a/clients/js/src/confidentialTransferHelpers.ts b/clients/js/src/confidentialTransferHelpers.ts new file mode 100644 index 000000000..60abd6fba --- /dev/null +++ b/clients/js/src/confidentialTransferHelpers.ts @@ -0,0 +1,596 @@ +import { + closeContextStateProof, + verifyBatchedGroupedCiphertext3HandlesValidity, + verifyBatchedRangeProofU128, + verifyBatchedRangeProofU64, + verifyCiphertextCommitmentEquality, + verifyPubkeyValidity, +} from '@solana-program/zk-elgamal-proof'; +import { + Address, + Instruction, + TransactionSigner, + generateKeyPairSigner, + getAddressEncoder, + isSome, + nonDivisibleSequentialInstructionPlan, + parallelInstructionPlan, + sequentialInstructionPlan, + type GetMinimumBalanceForRentExemptionApi, + type InstructionPlan, + type ReadonlyUint8Array, + type Rpc, +} from '@solana/kit'; +import { + ExtensionType, + Extension, + TOKEN_2022_PROGRAM_ADDRESS, + Token, + findAssociatedTokenPda, + getApplyConfidentialPendingBalanceInstruction, + getConfidentialTransferInstruction, + getConfidentialWithdrawInstruction, + getConfigureConfidentialTransferAccountInstruction, + getCreateAssociatedTokenIdempotentInstruction, + getReallocateInstruction, +} from './generated'; +import { + extractCiphertextFromGroupedBytes, + subtractAmountFromCiphertext, + subtractWithLoHiCiphertexts, +} from './confidentialTransferArithmetic'; + +const DEFAULT_MAXIMUM_PENDING_BALANCE_CREDIT_COUNTER = 1n << 16n; +const PENDING_BALANCE_LO_BIT_LENGTH = 16n; +const TRANSFER_AMOUNT_LO_BIT_LENGTH = 16n; +const TRANSFER_AMOUNT_HI_BIT_LENGTH = 32n; +const REMAINING_BALANCE_BIT_LENGTH = 64; +const RANGE_PROOF_PADDING_BIT_LENGTH = 16; + +import type * as ZkSdk from '@solana/zk-sdk/node'; + +/** The runtime shape of `@solana/zk-sdk/node` — pass it as the `zk` parameter to every helper. */ +export type ConfidentialTransferZkClient = typeof ZkSdk; + +type ConfidentialTransferAccountExtension = Extract; + +type ContextStateProofMode = { + proofMode?: 'context-state'; + payer: TransactionSigner; + rpc: Rpc; +}; + +export type GetCreateConfidentialTransferAccountInstructionPlanInput = { + payer: TransactionSigner; + owner: Address | TransactionSigner; + mint: Address; + token?: Address; + authority?: Address | TransactionSigner; + rpc: Rpc; + zk: ConfidentialTransferZkClient; + elgamalKeypair: ZkSdk.ElGamalKeypair; + aesKey: ZkSdk.AeKey; + maximumPendingBalanceCreditCounter?: number | bigint; + multiSigners?: Array; + programAddress?: Address; +}; + +export type GetApplyConfidentialPendingBalanceInstructionFromTokenInput = { + token: Address; + tokenAccount: Token; + authority: Address | TransactionSigner; + zk: ConfidentialTransferZkClient; + elgamalSecretKey: ZkSdk.ElGamalSecretKey; + aesKey: ZkSdk.AeKey; + multiSigners?: Array; + programAddress?: Address; +}; + +type GetConfidentialWithdrawInstructionPlanBaseInput = { + token: Address; + mint: Address; + tokenAccount: Token; + authority: Address | TransactionSigner; + amount: number | bigint; + decimals: number; + zk: ConfidentialTransferZkClient; + elgamalKeypair: ZkSdk.ElGamalKeypair; + aesKey: ZkSdk.AeKey; + multiSigners?: Array; + programAddress?: Address; +}; + +export type GetConfidentialWithdrawInstructionPlanInput = GetConfidentialWithdrawInstructionPlanBaseInput & + ContextStateProofMode; + +type GetConfidentialTransferInstructionPlanBaseInput = { + sourceToken: Address; + mint: Address; + destinationToken: Address; + sourceTokenAccount: Token; + auditorElgamalPubkey?: Address; + authority: Address | TransactionSigner; + amount: number | bigint; + zk: ConfidentialTransferZkClient; + sourceElgamalKeypair: ZkSdk.ElGamalKeypair; + aesKey: ZkSdk.AeKey; + multiSigners?: Array; + programAddress?: Address; +} & ( + | { destinationTokenAccount: Token; destinationElgamalPubkey?: Address } + | { destinationElgamalPubkey: Address; destinationTokenAccount?: never } +); + +export type GetConfidentialTransferInstructionPlanInput = GetConfidentialTransferInstructionPlanBaseInput & + ContextStateProofMode; + +function getTokenProgramAddress(programAddress?: Address) { + return programAddress ?? TOKEN_2022_PROGRAM_ADDRESS; +} + +function addressOf(value: Address | TransactionSigner): Address { + return isSigner(value) ? value.address : value; +} + +function isSigner(value: Address | TransactionSigner): value is TransactionSigner { + return typeof value !== 'string'; +} + +function getRequiredConfidentialTransferAccountExtension(tokenAccount: Token): ConfidentialTransferAccountExtension { + if (!isSome(tokenAccount.extensions)) { + throw new Error('Token account is missing extensions.'); + } + + const extension = tokenAccount.extensions.value.find( + candidate => candidate.__kind === 'ConfidentialTransferAccount', + ) as ConfidentialTransferAccountExtension | undefined; + if (!extension) { + throw new Error('Token account is missing the ConfidentialTransferAccount extension.'); + } + + return extension; +} + +function parseAeCiphertext(zk: ConfidentialTransferZkClient, bytes: ReadonlyUint8Array) { + const ciphertext = zk.AeCiphertext.fromBytes(new Uint8Array(bytes)); + if (!ciphertext) { + throw new Error('Failed to deserialize an authenticated-encryption ciphertext.'); + } + return ciphertext; +} + +function parseElGamalCiphertext(zk: ConfidentialTransferZkClient, bytes: ReadonlyUint8Array) { + const ciphertext = zk.ElGamalCiphertext.fromBytes(new Uint8Array(bytes)); + if (!ciphertext) { + throw new Error('Failed to deserialize an ElGamal ciphertext.'); + } + return ciphertext; +} + +function getElGamalPubkeyFromAddress(zk: ConfidentialTransferZkClient, value: Address) { + return zk.ElGamalPubkey.fromBytes(getAddressEncoder().encode(value) as Uint8Array); +} + +function getDefaultAuditorElGamalPubkey(zk: ConfidentialTransferZkClient) { + return zk.ElGamalPubkey.fromBytes(new Uint8Array(32)); +} + +function getDestinationElGamalPubkey(input: GetConfidentialTransferInstructionPlanInput) { + if (input.destinationElgamalPubkey) { + return getElGamalPubkeyFromAddress(input.zk, input.destinationElgamalPubkey); + } + if (!input.destinationTokenAccount) { + throw new Error('Destination confidential transfer state is required.'); + } + + return getElGamalPubkeyFromAddress( + input.zk, + getRequiredConfidentialTransferAccountExtension(input.destinationTokenAccount).elgamalPubkey, + ); +} + +function splitAmount(amount: bigint, bitLength: bigint): [bigint, bigint] { + const mask = (1n << bitLength) - 1n; + return [amount & mask, amount >> bitLength]; +} + +function combineBalances(balanceLo: bigint, balanceHi: bigint) { + return (balanceHi << PENDING_BALANCE_LO_BIT_LENGTH) + balanceLo; +} + +function decryptAvailableBalance( + zk: ConfidentialTransferZkClient, + account: ConfidentialTransferAccountExtension, + aesKey: ZkSdk.AeKey, +) { + return aesKey.decrypt(parseAeCiphertext(zk, account.decryptableAvailableBalance)); +} + +function assertInstructionDataProofModeIsUnsupported(input: { proofMode?: string }) { + if (input.proofMode === 'instruction-data') { + throw new Error( + 'instruction-data proof mode is unsupported for confidential withdraw/transfer helpers in clients/js; use the default context-state flow.', + ); + } +} + +function assertCreateHelperOwnerMatchesAuthority( + owner: Address | TransactionSigner, + authority: Address | TransactionSigner, +) { + if (addressOf(owner) !== addressOf(authority)) { + throw new Error( + 'This helper is scoped to the token-account owner. For the ATA convenience flow, authority must match owner.', + ); + } +} + +/** + * Builds the setup-and-cleanup instruction plans for a single proof's + * context-state account. The setup plan creates the context-state account + * and verifies the proof into it (these two instructions must share a + * transaction). The cleanup plan closes the context-state account to recover + * its rent. + */ +function assertNonNegativeAmount(amount: bigint): void { + if (amount < 0n) { + throw new Error('Amount must be non-negative.'); + } +} + +function computeNewAvailableBalance(currentBalance: bigint, amount: bigint): bigint { + assertNonNegativeAmount(amount); + const newBalance = currentBalance - amount; + if (newBalance < 0n) { + throw new Error('Insufficient funds.'); + } + return newBalance; +} + +async function buildContextStateProofPlan( + proofData: ReadonlyUint8Array, + verifyAction: (args: { + rpc: Rpc; + payer: TransactionSigner; + proofData: Uint8Array; + contextState: { contextAccount: Awaited>; authority: Address }; + }) => Promise, + payer: TransactionSigner, + rpc: Rpc, + contextStateAuthority: TransactionSigner = payer, +): Promise<{ address: Address; setup: InstructionPlan; cleanup: Instruction }> { + const contextAccount = await generateKeyPairSigner(); + const setupInstructions = await verifyAction({ + rpc, + payer, + proofData: new Uint8Array(proofData), + contextState: { contextAccount, authority: contextStateAuthority.address }, + }); + return { + address: contextAccount.address, + // Divisible: the create-account and verify-proof instructions can fit + // in one transaction for small proofs (e.g. PubkeyValidity) but exceed + // the size limit for larger proofs (e.g. BatchedRangeProofU128). A + // transaction planner decides how to pack them; the verify only needs + // the account to exist, which is true once create-account is confirmed. + setup: sequentialInstructionPlan(setupInstructions), + cleanup: closeContextStateProof({ + contextState: contextAccount.address, + authority: contextStateAuthority, + destination: payer.address, + }), + }; +} + +/** + * Returns a single-transaction plan that creates the ATA, reallocates it + * for the confidential-transfer extension, configures the account, and + * verifies the ZK pubkey-validity proof. + */ +export async function getCreateConfidentialTransferAccountInstructionPlan( + input: GetCreateConfidentialTransferAccountInstructionPlanInput, +): Promise { + const programAddress = getTokenProgramAddress(input.programAddress); + const authority = input.authority ?? input.owner; + assertCreateHelperOwnerMatchesAuthority(input.owner, authority); + + const ownerAddress = addressOf(input.owner); + const token = + input.token ?? + ( + await findAssociatedTokenPda({ + owner: ownerAddress, + tokenProgram: programAddress, + mint: input.mint, + }) + )[0]; + + const pubkeyValidityProofData = new input.zk.PubkeyValidityProofData(input.elgamalKeypair); + const [verifyProofInstruction] = await verifyPubkeyValidity({ + rpc: input.rpc, + payer: input.payer, + proofData: new Uint8Array(pubkeyValidityProofData.toBytes()), + }); + + return nonDivisibleSequentialInstructionPlan([ + getCreateAssociatedTokenIdempotentInstruction({ + ata: token, + mint: input.mint, + owner: ownerAddress, + payer: input.payer, + tokenProgram: programAddress, + }), + getReallocateInstruction( + { + token, + payer: input.payer, + owner: authority, + newExtensionTypes: [ExtensionType.ConfidentialTransferAccount], + multiSigners: input.multiSigners, + }, + { programAddress }, + ), + getConfigureConfidentialTransferAccountInstruction( + { + token, + mint: input.mint, + authority, + decryptableZeroBalance: input.aesKey.encrypt(0n).toBytes(), + maximumPendingBalanceCreditCounter: + input.maximumPendingBalanceCreditCounter ?? DEFAULT_MAXIMUM_PENDING_BALANCE_CREDIT_COUNTER, + proofInstructionOffset: 1, + multiSigners: input.multiSigners, + }, + { programAddress }, + ), + verifyProofInstruction, + ]); +} + +/** + * Builds an `ApplyPendingBalance` instruction plan from a decoded token + * account, decrypting the pending balance and re-encrypting the new + * available balance locally. + */ +export function getApplyConfidentialPendingBalanceInstructionFromToken( + input: GetApplyConfidentialPendingBalanceInstructionFromTokenInput, +): Instruction { + const account = getRequiredConfidentialTransferAccountExtension(input.tokenAccount); + const pendingBalanceLo = input.elgamalSecretKey.decrypt( + parseElGamalCiphertext(input.zk, account.pendingBalanceLow), + ); + const pendingBalanceHi = input.elgamalSecretKey.decrypt( + parseElGamalCiphertext(input.zk, account.pendingBalanceHigh), + ); + const newDecryptableAvailableBalance = input.aesKey + .encrypt( + decryptAvailableBalance(input.zk, account, input.aesKey) + + combineBalances(pendingBalanceLo, pendingBalanceHi), + ) + .toBytes(); + + return getApplyConfidentialPendingBalanceInstruction( + { + token: input.token, + authority: input.authority, + expectedPendingBalanceCreditCounter: account.pendingBalanceCreditCounter, + newDecryptableAvailableBalance, + multiSigners: input.multiSigners, + }, + { programAddress: getTokenProgramAddress(input.programAddress) }, + ); +} + +/** + * Returns an instruction plan that moves tokens from the encrypted + * available balance back to the plaintext balance. Generates and verifies + * the equality and batched range proofs via context-state accounts. + */ +export async function getConfidentialWithdrawInstructionPlan( + input: GetConfidentialWithdrawInstructionPlanInput, +): Promise { + assertInstructionDataProofModeIsUnsupported(input as { proofMode?: string }); + const account = getRequiredConfidentialTransferAccountExtension(input.tokenAccount); + const amount = BigInt(input.amount); + assertNonNegativeAmount(amount); + const newAvailableBalance = computeNewAvailableBalance( + decryptAvailableBalance(input.zk, account, input.aesKey), + amount, + ); + + const remainingBalanceOpening = new input.zk.PedersenOpening(); + const remainingBalanceCommitment = input.zk.PedersenCommitment.from(newAvailableBalance, remainingBalanceOpening); + const remainingBalanceCiphertext = parseElGamalCiphertext( + input.zk, + subtractAmountFromCiphertext(account.availableBalance, amount), + ); + + const equalityProofData = new input.zk.CiphertextCommitmentEqualityProofData( + input.elgamalKeypair, + remainingBalanceCiphertext, + remainingBalanceCommitment, + remainingBalanceOpening, + newAvailableBalance, + ); + const rangeProofData = new input.zk.BatchedRangeProofU64Data( + [remainingBalanceCommitment], + new BigUint64Array([newAvailableBalance]), + Uint8Array.from([REMAINING_BALANCE_BIT_LENGTH]), + [remainingBalanceOpening], + ); + + const [equalityProofPlan, rangeProofPlan] = await Promise.all([ + buildContextStateProofPlan( + equalityProofData.toBytes(), + verifyCiphertextCommitmentEquality, + input.payer, + input.rpc, + ), + buildContextStateProofPlan(rangeProofData.toBytes(), verifyBatchedRangeProofU64, input.payer, input.rpc), + ]); + + return sequentialInstructionPlan([ + parallelInstructionPlan([equalityProofPlan.setup, rangeProofPlan.setup]), + getConfidentialWithdrawInstruction( + { + token: input.token, + mint: input.mint, + equalityRecord: equalityProofPlan.address, + rangeRecord: rangeProofPlan.address, + authority: input.authority, + amount, + decimals: input.decimals, + newDecryptableAvailableBalance: input.aesKey.encrypt(newAvailableBalance).toBytes(), + equalityProofInstructionOffset: 0, + rangeProofInstructionOffset: 0, + multiSigners: input.multiSigners, + }, + { programAddress: input.programAddress ?? TOKEN_2022_PROGRAM_ADDRESS }, + ), + parallelInstructionPlan([equalityProofPlan.cleanup, rangeProofPlan.cleanup]), + ]); +} + +/** + * Returns an instruction plan that confidentially transfers tokens between + * two accounts. Splits the amount into lo/hi halves and verifies the three + * required proofs (equality, grouped-ciphertext validity, batched range) + * via context-state accounts. + */ +export async function getConfidentialTransferInstructionPlan( + input: GetConfidentialTransferInstructionPlanInput, +): Promise { + assertInstructionDataProofModeIsUnsupported(input as { proofMode?: string }); + const sourceAccount = getRequiredConfidentialTransferAccountExtension(input.sourceTokenAccount); + const amount = BigInt(input.amount); + assertNonNegativeAmount(amount); + const [transferAmountLo, transferAmountHi] = splitAmount(amount, TRANSFER_AMOUNT_LO_BIT_LENGTH); + + const sourcePubkey = input.sourceElgamalKeypair.pubkey(); + const destinationPubkey = getDestinationElGamalPubkey(input); + const auditorPubkey = input.auditorElgamalPubkey + ? getElGamalPubkeyFromAddress(input.zk, input.auditorElgamalPubkey) + : getDefaultAuditorElGamalPubkey(input.zk); + + const openingLo = new input.zk.PedersenOpening(); + const openingHi = new input.zk.PedersenOpening(); + const groupedCiphertextLo = input.zk.GroupedElGamalCiphertext3Handles.encryptWith( + sourcePubkey, + destinationPubkey, + auditorPubkey, + transferAmountLo, + openingLo, + ); + const groupedCiphertextHi = input.zk.GroupedElGamalCiphertext3Handles.encryptWith( + sourcePubkey, + destinationPubkey, + auditorPubkey, + transferAmountHi, + openingHi, + ); + + const groupedCiphertextLoBytes = groupedCiphertextLo.toBytes(); + const groupedCiphertextHiBytes = groupedCiphertextHi.toBytes(); + const transferAmountSourceCiphertextLo = extractCiphertextFromGroupedBytes(groupedCiphertextLoBytes, 0); + const transferAmountSourceCiphertextHi = extractCiphertextFromGroupedBytes(groupedCiphertextHiBytes, 0); + const transferAmountAuditorCiphertextLo = extractCiphertextFromGroupedBytes(groupedCiphertextLoBytes, 2); + const transferAmountAuditorCiphertextHi = extractCiphertextFromGroupedBytes(groupedCiphertextHiBytes, 2); + + const newAvailableBalance = computeNewAvailableBalance( + decryptAvailableBalance(input.zk, sourceAccount, input.aesKey), + amount, + ); + const newAvailableBalanceOpening = new input.zk.PedersenOpening(); + const newAvailableBalanceCommitment = input.zk.PedersenCommitment.from( + newAvailableBalance, + newAvailableBalanceOpening, + ); + const newAvailableBalanceCiphertext = parseElGamalCiphertext( + input.zk, + subtractWithLoHiCiphertexts( + sourceAccount.availableBalance, + transferAmountSourceCiphertextLo, + transferAmountSourceCiphertextHi, + TRANSFER_AMOUNT_LO_BIT_LENGTH, + ), + ); + + const equalityProofData = new input.zk.CiphertextCommitmentEqualityProofData( + input.sourceElgamalKeypair, + newAvailableBalanceCiphertext, + newAvailableBalanceCommitment, + newAvailableBalanceOpening, + newAvailableBalance, + ); + const ciphertextValidityProofData = new input.zk.BatchedGroupedCiphertext3HandlesValidityProofData( + sourcePubkey, + destinationPubkey, + auditorPubkey, + groupedCiphertextLo, + groupedCiphertextHi, + transferAmountLo, + transferAmountHi, + openingLo, + openingHi, + ); + + const commitmentLo = input.zk.PedersenCommitment.fromBytes(groupedCiphertextLoBytes.slice(0, 32)); + const commitmentHi = input.zk.PedersenCommitment.fromBytes(groupedCiphertextHiBytes.slice(0, 32)); + const paddingOpening = new input.zk.PedersenOpening(); + const paddingCommitment = input.zk.PedersenCommitment.from(0n, paddingOpening); + const rangeProofData = new input.zk.BatchedRangeProofU128Data( + [newAvailableBalanceCommitment, commitmentLo, commitmentHi, paddingCommitment], + new BigUint64Array([newAvailableBalance, transferAmountLo, transferAmountHi, 0n]), + Uint8Array.from([ + REMAINING_BALANCE_BIT_LENGTH, + Number(TRANSFER_AMOUNT_LO_BIT_LENGTH), + Number(TRANSFER_AMOUNT_HI_BIT_LENGTH), + RANGE_PROOF_PADDING_BIT_LENGTH, + ]), + [newAvailableBalanceOpening, openingLo, openingHi, paddingOpening], + ); + + const [equalityProofPlan, ciphertextValidityProofPlan, rangeProofPlan] = await Promise.all([ + buildContextStateProofPlan( + equalityProofData.toBytes(), + verifyCiphertextCommitmentEquality, + input.payer, + input.rpc, + ), + buildContextStateProofPlan( + ciphertextValidityProofData.toBytes(), + verifyBatchedGroupedCiphertext3HandlesValidity, + input.payer, + input.rpc, + ), + buildContextStateProofPlan(rangeProofData.toBytes(), verifyBatchedRangeProofU128, input.payer, input.rpc), + ]); + + return sequentialInstructionPlan([ + parallelInstructionPlan([equalityProofPlan.setup, ciphertextValidityProofPlan.setup, rangeProofPlan.setup]), + getConfidentialTransferInstruction( + { + sourceToken: input.sourceToken, + mint: input.mint, + destinationToken: input.destinationToken, + equalityRecord: equalityProofPlan.address, + ciphertextValidityRecord: ciphertextValidityProofPlan.address, + rangeRecord: rangeProofPlan.address, + authority: input.authority, + newSourceDecryptableAvailableBalance: input.aesKey.encrypt(newAvailableBalance).toBytes(), + transferAmountAuditorCiphertextLo, + transferAmountAuditorCiphertextHi, + equalityProofInstructionOffset: 0, + ciphertextValidityProofInstructionOffset: 0, + rangeProofInstructionOffset: 0, + multiSigners: input.multiSigners, + }, + { programAddress: input.programAddress ?? TOKEN_2022_PROGRAM_ADDRESS }, + ), + parallelInstructionPlan([ + equalityProofPlan.cleanup, + ciphertextValidityProofPlan.cleanup, + rangeProofPlan.cleanup, + ]), + ]); +} diff --git a/clients/js/src/confidentialTransferKeys.ts b/clients/js/src/confidentialTransferKeys.ts new file mode 100644 index 000000000..8eebb9552 --- /dev/null +++ b/clients/js/src/confidentialTransferKeys.ts @@ -0,0 +1,108 @@ +import { + createSignableMessage, + getAddressDecoder, + getAddressEncoder, + getTupleEncoder, + type Address, + type MessagePartialSigner, + type ReadonlyUint8Array, +} from '@solana/kit'; +import type { ConfidentialTransferZkClient } from './confidentialTransferHelpers'; + +export type DerivedElGamalKeypair = Readonly<{ + elgamalPubkey: Address; + secretKey: Uint8Array; +}>; + +async function signDerivationMessage(signer: MessagePartialSigner, message: Uint8Array): Promise { + const [signatures] = await signer.signMessages([createSignableMessage(message)]); + const signature = signatures?.[signer.address]; + if (signature == null) { + throw new Error(`Signer ${signer.address} did not return a signature`); + } + return new Uint8Array(signature); +} + +function ownerMintSeed(owner: Address, mint: Address): ReadonlyUint8Array { + return getTupleEncoder([getAddressEncoder(), getAddressEncoder()]).encode([owner, mint]); +} + +/** + * Derives an ElGamal keypair by having the signer sign a domain-separated + * message and feeding the resulting Ed25519 signature into the WASM ZK SDK. + */ +export async function deriveElGamalKeypair({ + signer, + zk, + publicSeed = new Uint8Array(0), +}: { + signer: MessagePartialSigner; + zk: ConfidentialTransferZkClient; + publicSeed?: ReadonlyUint8Array; +}): Promise { + const message = zk.ElGamalKeypair.signerMessage(new Uint8Array(publicSeed)); + const signature = await signDerivationMessage(signer, message); + const keypair = zk.ElGamalKeypair.fromSignature(signature); + const secretKey = new Uint8Array(keypair.secret().toBytes()); + const elgamalPubkey = getAddressDecoder().decode(new Uint8Array(keypair.pubkey().toBytes())); + return { elgamalPubkey, secretKey }; +} + +/** + * Derives an ElGamal keypair bound to an `(owner, mint)` pair. The seed + * is `concat(ownerBytes, mintBytes)`, which is stable across token-account + * close-and-reopen and prevents key reuse across mints. + */ +export async function deriveElGamalKeypairForOwnerMint({ + signer, + zk, + owner, + mint, +}: { + signer: MessagePartialSigner; + zk: ConfidentialTransferZkClient; + owner: Address; + mint: Address; +}): Promise { + return await deriveElGamalKeypair({ signer, zk, publicSeed: ownerMintSeed(owner, mint) }); +} + +/** + * Derives an AES-128 authenticated-encryption key by having the signer + * sign a domain-separated message and feeding the signature into the + * WASM ZK SDK. + */ +export async function deriveAeKey({ + signer, + zk, + publicSeed = new Uint8Array(0), +}: { + signer: MessagePartialSigner; + zk: ConfidentialTransferZkClient; + publicSeed?: ReadonlyUint8Array; +}): Promise { + const message = zk.AeKey.signerMessage(new Uint8Array(publicSeed)); + const signature = await signDerivationMessage(signer, message); + const aeKey = zk.AeKey.fromSignature(signature); + return new Uint8Array(aeKey.toBytes()); +} + +/** + * Derives an AES key scoped to an `(owner, mint)` pair. + * + * See `deriveElGamalKeypairForOwnerMint` for why this is the right binding + * for confidential token accounts. + */ +export async function deriveAeKeyForOwnerMint({ + signer, + zk, + owner, + mint, +}: { + signer: MessagePartialSigner; + zk: ConfidentialTransferZkClient; + owner: Address; + mint: Address; +}): Promise { + return await deriveAeKey({ signer, zk, publicSeed: ownerMintSeed(owner, mint) }); +} diff --git a/clients/js/src/generated/instructions/confidentialTransfer.ts b/clients/js/src/generated/instructions/confidentialTransfer.ts index 3e015861a..9aa271e48 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; @@ -57,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 & @@ -72,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, ] @@ -90,6 +110,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 +145,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 +183,8 @@ export function getConfidentialTransferInstructionDataEncoder(): FixedSizeEncode ['discriminator', getU8Encoder()], ['confidentialTransferDiscriminator', getU8Encoder()], ['newSourceDecryptableAvailableBalance', getDecryptableBalanceEncoder()], + ['transferAmountAuditorCiphertextLo', getEncryptedBalanceEncoder()], + ['transferAmountAuditorCiphertextHi', getEncryptedBalanceEncoder()], ['equalityProofInstructionOffset', getI8Encoder()], ['ciphertextValidityProofInstructionOffset', getI8Encoder()], ['rangeProofInstructionOffset', getI8Encoder()], @@ -160,6 +202,8 @@ export function getConfidentialTransferInstructionDataDecoder(): FixedSizeDecode ['discriminator', getU8Decoder()], ['confidentialTransferDiscriminator', getU8Decoder()], ['newSourceDecryptableAvailableBalance', getDecryptableBalanceDecoder()], + ['transferAmountAuditorCiphertextLo', getEncryptedBalanceDecoder()], + ['transferAmountAuditorCiphertextHi', getEncryptedBalanceDecoder()], ['equalityProofInstructionOffset', getI8Decoder()], ['ciphertextValidityProofInstructionOffset', getI8Decoder()], ['rangeProofInstructionOffset', getI8Decoder()], @@ -207,6 +251,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']; @@ -274,7 +320,7 @@ export function getConfidentialTransferInstruction< signer, })); - const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + const getAccountMeta = getAccountMetaFactory(programAddress, 'omitted'); return Object.freeze({ accounts: [ getAccountMeta(accounts.sourceToken), @@ -286,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< @@ -342,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'); } @@ -352,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/src/generated/instructions/configureConfidentialTransferAccount.ts b/clients/js/src/generated/instructions/configureConfidentialTransferAccount.ts index b41ecc09f..a6da7bfa0 100644 --- a/clients/js/src/generated/instructions/configureConfidentialTransferAccount.ts +++ b/clients/js/src/generated/instructions/configureConfidentialTransferAccount.ts @@ -60,7 +60,6 @@ export type ConfigureConfidentialTransferAccountInstruction< TAccountMint extends string | AccountMeta = string, TAccountInstructionsSysvarOrContextState extends string | AccountMeta = 'Sysvar1nstructions1111111111111111111111111', - TAccountRecord extends string | AccountMeta = string, TAccountAuthority extends string | AccountMeta = string, TRemainingAccounts extends readonly AccountMeta[] = [], > = Instruction & @@ -72,7 +71,6 @@ export type ConfigureConfidentialTransferAccountInstruction< TAccountInstructionsSysvarOrContextState extends string ? ReadonlyAccount : TAccountInstructionsSysvarOrContextState, - TAccountRecord extends string ? ReadonlyAccount : TAccountRecord, TAccountAuthority extends string ? ReadonlyAccount : TAccountAuthority, ...TRemainingAccounts, ] @@ -156,7 +154,6 @@ export type ConfigureConfidentialTransferAccountInput< TAccountToken extends string = string, TAccountMint extends string = string, TAccountInstructionsSysvarOrContextState extends string = string, - TAccountRecord extends string = string, TAccountAuthority extends string = string, > = { /** The SPL Token account. */ @@ -170,8 +167,6 @@ export type ConfigureConfidentialTransferAccountInput< * account. */ instructionsSysvarOrContextState?: Address; - /** (Optional) Record account if the accompanying proof is to be read from a record account. */ - record?: Address; /** The source account's owner/delegate or its multisignature account. */ authority: Address | TransactionSigner; decryptableZeroBalance: ConfigureConfidentialTransferAccountInstructionDataArgs['decryptableZeroBalance']; @@ -184,7 +179,6 @@ export function getConfigureConfidentialTransferAccountInstruction< TAccountToken extends string, TAccountMint extends string, TAccountInstructionsSysvarOrContextState extends string, - TAccountRecord extends string, TAccountAuthority extends string, TProgramAddress extends Address = typeof TOKEN_2022_PROGRAM_ADDRESS, >( @@ -192,7 +186,6 @@ export function getConfigureConfidentialTransferAccountInstruction< TAccountToken, TAccountMint, TAccountInstructionsSysvarOrContextState, - TAccountRecord, TAccountAuthority >, config?: { programAddress?: TProgramAddress }, @@ -201,7 +194,6 @@ export function getConfigureConfidentialTransferAccountInstruction< TAccountToken, TAccountMint, TAccountInstructionsSysvarOrContextState, - TAccountRecord, (typeof input)['authority'] extends TransactionSigner ? ReadonlySignerAccount & AccountSignerMeta : TAccountAuthority @@ -214,7 +206,6 @@ export function getConfigureConfidentialTransferAccountInstruction< token: { value: input.token ?? null, isWritable: true }, mint: { value: input.mint ?? null, isWritable: false }, instructionsSysvarOrContextState: { value: input.instructionsSysvarOrContextState ?? null, isWritable: false }, - record: { value: input.record ?? null, isWritable: false }, authority: { value: input.authority ?? null, isWritable: false }, }; const accounts = originalAccounts as Record; @@ -241,7 +232,6 @@ export function getConfigureConfidentialTransferAccountInstruction< getAccountMeta(accounts.token), getAccountMeta(accounts.mint), getAccountMeta(accounts.instructionsSysvarOrContextState), - getAccountMeta(accounts.record), getAccountMeta(accounts.authority), ...remainingAccounts, ], @@ -254,7 +244,6 @@ export function getConfigureConfidentialTransferAccountInstruction< TAccountToken, TAccountMint, TAccountInstructionsSysvarOrContextState, - TAccountRecord, (typeof input)['authority'] extends TransactionSigner ? ReadonlySignerAccount & AccountSignerMeta : TAccountAuthority @@ -278,10 +267,8 @@ export type ParsedConfigureConfidentialTransferAccountInstruction< * account. */ instructionsSysvarOrContextState: TAccountMetas[2]; - /** (Optional) Record account if the accompanying proof is to be read from a record account. */ - record?: TAccountMetas[3] | undefined; /** The source account's owner/delegate or its multisignature account. */ - authority: TAccountMetas[4]; + authority: TAccountMetas[3]; }; data: ConfigureConfidentialTransferAccountInstructionData; }; @@ -294,7 +281,7 @@ export function parseConfigureConfidentialTransferAccountInstruction< InstructionWithAccounts & InstructionWithData, ): ParsedConfigureConfidentialTransferAccountInstruction { - if (instruction.accounts.length < 5) { + if (instruction.accounts.length < 4) { // TODO: Coded error. throw new Error('Not enough accounts'); } @@ -304,17 +291,12 @@ export function parseConfigureConfidentialTransferAccountInstruction< accountIndex += 1; return accountMeta; }; - const getNextOptionalAccount = () => { - const accountMeta = getNextAccount(); - return accountMeta.address === TOKEN_2022_PROGRAM_ADDRESS ? undefined : accountMeta; - }; return { programAddress: instruction.programAddress, accounts: { token: getNextAccount(), mint: getNextAccount(), instructionsSysvarOrContextState: getNextAccount(), - record: getNextOptionalAccount(), authority: getNextAccount(), }, data: getConfigureConfidentialTransferAccountInstructionDataDecoder().decode(instruction.data), diff --git a/clients/js/src/generated/instructions/configureConfidentialTransferAccountWithRegistry.ts b/clients/js/src/generated/instructions/configureConfidentialTransferAccountWithRegistry.ts new file mode 100644 index 000000000..b2f34ee9b --- /dev/null +++ b/clients/js/src/generated/instructions/configureConfidentialTransferAccountWithRegistry.ts @@ -0,0 +1,258 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + combineCodec, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlyAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, + type WritableSignerAccount, +} from '@solana/kit'; +import { TOKEN_2022_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const CONFIGURE_CONFIDENTIAL_TRANSFER_ACCOUNT_WITH_REGISTRY_DISCRIMINATOR = 27; + +export function getConfigureConfidentialTransferAccountWithRegistryDiscriminatorBytes() { + return getU8Encoder().encode(CONFIGURE_CONFIDENTIAL_TRANSFER_ACCOUNT_WITH_REGISTRY_DISCRIMINATOR); +} + +export const CONFIGURE_CONFIDENTIAL_TRANSFER_ACCOUNT_WITH_REGISTRY_CONFIDENTIAL_TRANSFER_DISCRIMINATOR = 14; + +export function getConfigureConfidentialTransferAccountWithRegistryConfidentialTransferDiscriminatorBytes() { + return getU8Encoder().encode( + CONFIGURE_CONFIDENTIAL_TRANSFER_ACCOUNT_WITH_REGISTRY_CONFIDENTIAL_TRANSFER_DISCRIMINATOR, + ); +} + +export type ConfigureConfidentialTransferAccountWithRegistryInstruction< + TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, + TAccountToken extends string | AccountMeta = string, + TAccountMint extends string | AccountMeta = string, + TAccountElgamalRegistry extends string | AccountMeta = string, + TAccountPayer extends string | AccountMeta | undefined = undefined, + TAccountSystemProgram extends string | AccountMeta | undefined = undefined, + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountToken extends string ? WritableAccount : TAccountToken, + TAccountMint extends string ? ReadonlyAccount : TAccountMint, + TAccountElgamalRegistry extends string ? ReadonlyAccount : TAccountElgamalRegistry, + ...(TAccountPayer extends undefined + ? [] + : [ + TAccountPayer extends string + ? WritableSignerAccount & AccountSignerMeta + : TAccountPayer, + ]), + ...(TAccountSystemProgram extends undefined + ? [] + : [ + TAccountSystemProgram extends string + ? ReadonlyAccount + : TAccountSystemProgram, + ]), + ...TRemainingAccounts, + ] + >; + +export type ConfigureConfidentialTransferAccountWithRegistryInstructionData = { + discriminator: number; + confidentialTransferDiscriminator: number; +}; + +export type ConfigureConfidentialTransferAccountWithRegistryInstructionDataArgs = {}; + +export function getConfigureConfidentialTransferAccountWithRegistryInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', getU8Encoder()], + ['confidentialTransferDiscriminator', getU8Encoder()], + ]), + value => ({ + ...value, + discriminator: CONFIGURE_CONFIDENTIAL_TRANSFER_ACCOUNT_WITH_REGISTRY_DISCRIMINATOR, + confidentialTransferDiscriminator: + CONFIGURE_CONFIDENTIAL_TRANSFER_ACCOUNT_WITH_REGISTRY_CONFIDENTIAL_TRANSFER_DISCRIMINATOR, + }), + ); +} + +export function getConfigureConfidentialTransferAccountWithRegistryInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([ + ['discriminator', getU8Decoder()], + ['confidentialTransferDiscriminator', getU8Decoder()], + ]); +} + +export function getConfigureConfidentialTransferAccountWithRegistryInstructionDataCodec(): FixedSizeCodec< + ConfigureConfidentialTransferAccountWithRegistryInstructionDataArgs, + ConfigureConfidentialTransferAccountWithRegistryInstructionData +> { + return combineCodec( + getConfigureConfidentialTransferAccountWithRegistryInstructionDataEncoder(), + getConfigureConfidentialTransferAccountWithRegistryInstructionDataDecoder(), + ); +} + +export type ConfigureConfidentialTransferAccountWithRegistryInput< + TAccountToken extends string = string, + TAccountMint extends string = string, + TAccountElgamalRegistry extends string = string, + TAccountPayer extends string = string, + TAccountSystemProgram extends string = string, +> = { + /** The SPL Token account to configure. */ + token: Address; + /** The corresponding SPL Token mint. */ + mint: Address; + /** The ElGamal registry account that provides the encryption pubkey for the owner. */ + elgamalRegistry: Address; + /** + * (Optional) Payer that funds the rent required to reallocate the token + * account when it does not yet have the `ConfidentialTransferAccount` extension. + */ + payer?: TransactionSigner; + /** (Optional) System program, required when `payer` is provided. */ + systemProgram?: Address; +}; + +export function getConfigureConfidentialTransferAccountWithRegistryInstruction< + TAccountToken extends string, + TAccountMint extends string, + TAccountElgamalRegistry extends string, + TAccountPayer extends string, + TAccountSystemProgram extends string, + TProgramAddress extends Address = typeof TOKEN_2022_PROGRAM_ADDRESS, +>( + input: ConfigureConfidentialTransferAccountWithRegistryInput< + TAccountToken, + TAccountMint, + TAccountElgamalRegistry, + TAccountPayer, + TAccountSystemProgram + >, + config?: { programAddress?: TProgramAddress }, +): ConfigureConfidentialTransferAccountWithRegistryInstruction< + TProgramAddress, + TAccountToken, + TAccountMint, + TAccountElgamalRegistry, + TAccountPayer, + TAccountSystemProgram +> { + // Program address. + const programAddress = config?.programAddress ?? TOKEN_2022_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + token: { value: input.token ?? null, isWritable: true }, + mint: { value: input.mint ?? null, isWritable: false }, + elgamalRegistry: { value: input.elgamalRegistry ?? null, isWritable: false }, + payer: { value: input.payer ?? null, isWritable: true }, + systemProgram: { value: input.systemProgram ?? null, isWritable: false }, + }; + const accounts = originalAccounts as Record; + + const getAccountMeta = getAccountMetaFactory(programAddress, 'omitted'); + return Object.freeze({ + accounts: [ + getAccountMeta(accounts.token), + getAccountMeta(accounts.mint), + getAccountMeta(accounts.elgamalRegistry), + getAccountMeta(accounts.payer), + getAccountMeta(accounts.systemProgram), + ].filter((x: T | undefined): x is T => x !== undefined), + data: getConfigureConfidentialTransferAccountWithRegistryInstructionDataEncoder().encode({}), + programAddress, + } as ConfigureConfidentialTransferAccountWithRegistryInstruction< + TProgramAddress, + TAccountToken, + TAccountMint, + TAccountElgamalRegistry, + TAccountPayer, + TAccountSystemProgram + >); +} + +export type ParsedConfigureConfidentialTransferAccountWithRegistryInstruction< + TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address; + accounts: { + /** The SPL Token account to configure. */ + token: TAccountMetas[0]; + /** The corresponding SPL Token mint. */ + mint: TAccountMetas[1]; + /** The ElGamal registry account that provides the encryption pubkey for the owner. */ + elgamalRegistry: TAccountMetas[2]; + /** + * (Optional) Payer that funds the rent required to reallocate the token + * account when it does not yet have the `ConfidentialTransferAccount` extension. + */ + payer?: TAccountMetas[3] | undefined; + /** (Optional) System program, required when `payer` is provided. */ + systemProgram?: TAccountMetas[4] | undefined; + }; + data: ConfigureConfidentialTransferAccountWithRegistryInstructionData; +}; + +export function parseConfigureConfidentialTransferAccountWithRegistryInstruction< + TProgram extends string, + TAccountMetas extends readonly AccountMeta[], +>( + instruction: Instruction & + InstructionWithAccounts & + InstructionWithData, +): ParsedConfigureConfidentialTransferAccountWithRegistryInstruction { + if (instruction.accounts.length < 3) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + let optionalAccountsRemaining = instruction.accounts.length - 3; + const getNextOptionalAccount = () => { + if (optionalAccountsRemaining === 0) return undefined; + optionalAccountsRemaining -= 1; + return getNextAccount(); + }; + return { + programAddress: instruction.programAddress, + accounts: { + token: getNextAccount(), + mint: getNextAccount(), + elgamalRegistry: getNextAccount(), + payer: getNextOptionalAccount(), + systemProgram: getNextOptionalAccount(), + }, + data: getConfigureConfidentialTransferAccountWithRegistryInstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/js/src/generated/instructions/index.ts b/clients/js/src/generated/instructions/index.ts index 730b5440a..10fd4a79c 100644 --- a/clients/js/src/generated/instructions/index.ts +++ b/clients/js/src/generated/instructions/index.ts @@ -19,6 +19,7 @@ export * from './confidentialTransfer'; export * from './confidentialTransferWithFee'; export * from './confidentialWithdraw'; export * from './configureConfidentialTransferAccount'; +export * from './configureConfidentialTransferAccountWithRegistry'; export * from './createAssociatedToken'; export * from './createAssociatedTokenIdempotent'; export * from './createNativeMint'; diff --git a/clients/js/src/generated/programs/token2022.ts b/clients/js/src/generated/programs/token2022.ts index 4baebcfe1..7897a5484 100644 --- a/clients/js/src/generated/programs/token2022.ts +++ b/clients/js/src/generated/programs/token2022.ts @@ -29,6 +29,7 @@ import { parseConfidentialTransferWithFeeInstruction, parseConfidentialWithdrawInstruction, parseConfigureConfidentialTransferAccountInstruction, + parseConfigureConfidentialTransferAccountWithRegistryInstruction, parseCreateNativeMintInstruction, parseDisableConfidentialCreditsInstruction, parseDisableCpiGuardInstruction, @@ -120,6 +121,7 @@ import { type ParsedConfidentialTransferWithFeeInstruction, type ParsedConfidentialWithdrawInstruction, type ParsedConfigureConfidentialTransferAccountInstruction, + type ParsedConfigureConfidentialTransferAccountWithRegistryInstruction, type ParsedCreateNativeMintInstruction, type ParsedDisableConfidentialCreditsInstruction, type ParsedDisableCpiGuardInstruction, @@ -259,6 +261,7 @@ export enum Token2022Instruction { InitializeConfidentialTransferMint, UpdateConfidentialTransferMint, ConfigureConfidentialTransferAccount, + ConfigureConfidentialTransferAccountWithRegistry, ApproveConfidentialTransferAccount, EmptyConfidentialTransferAccount, ConfidentialDeposit, @@ -426,6 +429,9 @@ export function identifyToken2022Instruction( if (containsBytes(data, getU8Encoder().encode(27), 0) && containsBytes(data, getU8Encoder().encode(2), 1)) { return Token2022Instruction.ConfigureConfidentialTransferAccount; } + if (containsBytes(data, getU8Encoder().encode(27), 0) && containsBytes(data, getU8Encoder().encode(14), 1)) { + return Token2022Instruction.ConfigureConfidentialTransferAccountWithRegistry; + } if (containsBytes(data, getU8Encoder().encode(27), 0) && containsBytes(data, getU8Encoder().encode(3), 1)) { return Token2022Instruction.ApproveConfidentialTransferAccount; } @@ -653,6 +659,9 @@ export type ParsedToken2022Instruction { + const signer = await generateKeyPairSigner(); + + const { elgamalPubkey, secretKey } = await deriveElGamalKeypair({ signer, zk }); + + t.truthy(elgamalPubkey); + t.is(secretKey.length, 32); + t.is(ADDRESS_ENCODER.encode(elgamalPubkey).length, 32); +}); + +test('it derives a 16-byte AES key', async t => { + const signer = await generateKeyPairSigner(); + + const aeKey = await deriveAeKey({ signer, zk }); + + t.is(aeKey.length, 16); +}); + +test('it derives deterministic ElGamal keys from the same signer and seed', async t => { + const signer = await generateKeyPairSigner(); + const publicSeed = new Uint8Array([1, 2, 3, 4]); + + const first = await deriveElGamalKeypair({ signer, zk, publicSeed }); + const second = await deriveElGamalKeypair({ signer, zk, publicSeed }); + + t.deepEqual(first.secretKey, second.secretKey); + t.is(first.elgamalPubkey, second.elgamalPubkey); +}); + +test('it derives deterministic AES keys from the same signer and seed', async t => { + const signer = await generateKeyPairSigner(); + const publicSeed = new Uint8Array([5, 6, 7, 8]); + + const first = await deriveAeKey({ signer, zk, publicSeed }); + const second = await deriveAeKey({ signer, zk, publicSeed }); + + t.deepEqual(first, second); +}); + +test('it derives different ElGamal keys for different seeds', async t => { + const signer = await generateKeyPairSigner(); + + const noSeed = await deriveElGamalKeypair({ signer, zk }); + const withSeed = await deriveElGamalKeypair({ signer, zk, publicSeed: new Uint8Array([1]) }); + + t.notDeepEqual(noSeed.secretKey, withSeed.secretKey); + t.not(noSeed.elgamalPubkey, withSeed.elgamalPubkey); +}); + +test('it derives different AES keys for different seeds', async t => { + const signer = await generateKeyPairSigner(); + + const noSeed = await deriveAeKey({ signer, zk }); + const withSeed = await deriveAeKey({ signer, zk, publicSeed: new Uint8Array([1]) }); + + t.notDeepEqual(noSeed, withSeed); +}); + +test('it derives different keys for different signers', async t => { + const [signerA, signerB] = await Promise.all([generateKeyPairSigner(), generateKeyPairSigner()]); + + const [elgamalA, elgamalB] = await Promise.all([ + deriveElGamalKeypair({ signer: signerA, zk }), + deriveElGamalKeypair({ signer: signerB, zk }), + ]); + const [aeA, aeB] = await Promise.all([deriveAeKey({ signer: signerA, zk }), deriveAeKey({ signer: signerB, zk })]); + + t.notDeepEqual(elgamalA.secretKey, elgamalB.secretKey); + t.notDeepEqual(aeA, aeB); +}); + +test('it matches the Rust solana-zk-sdk derivation vector', async t => { + const signer = await createKeyPairSignerFromPrivateKeyBytes(RUST_VECTOR_PRIVATE_KEY); + + const [derivedElGamal, derivedAeKey] = await Promise.all([ + deriveElGamalKeypair({ signer, zk, publicSeed: RUST_VECTOR_PUBLIC_SEED }), + deriveAeKey({ signer, zk, publicSeed: RUST_VECTOR_PUBLIC_SEED }), + ]); + + t.deepEqual(derivedElGamal.secretKey, RUST_VECTOR_ELGAMAL_SECRET_KEY); + t.is(derivedElGamal.elgamalPubkey, ADDRESS_DECODER.decode(RUST_VECTOR_ELGAMAL_PUBKEY)); + t.deepEqual(derivedAeKey, RUST_VECTOR_AE_KEY); +}); + +test('deriveElGamalKeypairForOwnerMint composes the seed as concat(owner, mint)', async t => { + const [signer, ownerSigner, mintSigner] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + const owner = ownerSigner.address; + const mint = mintSigner.address; + + const expectedSeed = new Uint8Array(64); + expectedSeed.set(ADDRESS_ENCODER.encode(owner), 0); + expectedSeed.set(ADDRESS_ENCODER.encode(mint), 32); + + const [convenience, manual] = await Promise.all([ + deriveElGamalKeypairForOwnerMint({ signer, zk, owner, mint }), + deriveElGamalKeypair({ signer, zk, publicSeed: expectedSeed }), + ]); + + t.deepEqual(convenience.secretKey, manual.secretKey); + t.is(convenience.elgamalPubkey, manual.elgamalPubkey); +}); + +test('deriveAeKeyForOwnerMint composes the seed as concat(owner, mint)', async t => { + const [signer, ownerSigner, mintSigner] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + const owner = ownerSigner.address; + const mint = mintSigner.address; + + const expectedSeed = new Uint8Array(64); + expectedSeed.set(ADDRESS_ENCODER.encode(owner), 0); + expectedSeed.set(ADDRESS_ENCODER.encode(mint), 32); + + const [convenience, manual] = await Promise.all([ + deriveAeKeyForOwnerMint({ signer, zk, owner, mint }), + deriveAeKey({ signer, zk, publicSeed: expectedSeed }), + ]); + + t.deepEqual(convenience, manual); +}); + +test('deriveElGamalKeypairForOwnerMint binds keys to (owner, mint), not just owner', async t => { + const [signer, mintA, mintB] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + const owner = signer.address; + + const [keysForMintA, keysForMintB] = await Promise.all([ + deriveElGamalKeypairForOwnerMint({ signer, zk, owner, mint: mintA.address }), + deriveElGamalKeypairForOwnerMint({ signer, zk, owner, mint: mintB.address }), + ]); + + // Different mints with the same owner must yield different keys. + t.notDeepEqual(keysForMintA.secretKey, keysForMintB.secretKey); + t.not(keysForMintA.elgamalPubkey, keysForMintB.elgamalPubkey); +}); + +test('deriveElGamalKeypairForOwnerMint binds keys to (owner, mint), not just mint', async t => { + const [signerA, signerB, mintSigner] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + const mint = mintSigner.address; + + const [keysForOwnerA, keysForOwnerB] = await Promise.all([ + deriveElGamalKeypairForOwnerMint({ signer: signerA, zk, owner: signerA.address, mint }), + deriveElGamalKeypairForOwnerMint({ signer: signerB, zk, owner: signerB.address, mint }), + ]); + + // Different owners with the same mint must yield different keys. + t.notDeepEqual(keysForOwnerA.secretKey, keysForOwnerB.secretKey); + t.not(keysForOwnerA.elgamalPubkey, keysForOwnerB.elgamalPubkey); +}); + +test('it derives keys from a generic message signer', async t => { + const signer = await generateKeyPairSigner(); + const genericSigner: MessagePartialSigner = { + address: signer.address, + signMessages: signer.signMessages, + }; + const publicSeed = new Uint8Array([9, 8, 7, 6]); + + const [derivedElGamal, expectedElGamal] = await Promise.all([ + deriveElGamalKeypair({ signer: genericSigner, zk, publicSeed }), + deriveElGamalKeypair({ signer, zk, publicSeed }), + ]); + const [derivedAeKey, expectedAeKey] = await Promise.all([ + deriveAeKey({ signer: genericSigner, zk, publicSeed }), + deriveAeKey({ signer, zk, publicSeed }), + ]); + + t.deepEqual(derivedElGamal.secretKey, expectedElGamal.secretKey); + t.is(derivedElGamal.elgamalPubkey, expectedElGamal.elgamalPubkey); + t.deepEqual(derivedAeKey, expectedAeKey); +}); + +test('it plugs derived ElGamal pubkeys directly into confidential transfer instruction builders', async t => { + const [authority, mintSigner] = await Promise.all([generateKeyPairSigner(), generateKeyPairSigner()]); + const derivedElGamal = await deriveElGamalKeypairForOwnerMint({ + signer: authority, + zk, + owner: authority.address, + mint: mintSigner.address, + }); + + const instruction = getInitializeConfidentialTransferMintInstruction({ + mint: mintSigner.address, + authority: some(authority.address), + autoApproveNewAccounts: true, + auditorElgamalPubkey: some(derivedElGamal.elgamalPubkey), + }); + const parsed = parseInitializeConfidentialTransferMintInstruction(instruction); + + t.deepEqual(parsed.data.authority, some(authority.address)); + t.true(parsed.data.autoApproveNewAccounts); + t.deepEqual(parsed.data.auditorElgamalPubkey, some(derivedElGamal.elgamalPubkey)); +}); + +test('it produces a non-zero ElGamal secret key', async t => { + const signer = await generateKeyPairSigner(); + + const { secretKey } = await deriveElGamalKeypair({ signer, zk }); + + t.false(secretKey.every(b => b === 0)); +}); + +test('it produces a non-zero AES key', async t => { + const signer = await generateKeyPairSigner(); + + const aeKey = await deriveAeKey({ signer, zk }); + + t.false(aeKey.every(b => b === 0)); +}); 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..ef3664e99 --- /dev/null +++ b/clients/js/test/extensions/confidentialTransfer/confidentialTransfer.test.ts @@ -0,0 +1,178 @@ +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); + +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: 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 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, 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, + }); + + // 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); +}); + +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/confidentialTransferHelpers.test.ts b/clients/js/test/extensions/confidentialTransfer/confidentialTransferHelpers.test.ts new file mode 100644 index 000000000..f0df40f05 --- /dev/null +++ b/clients/js/test/extensions/confidentialTransfer/confidentialTransferHelpers.test.ts @@ -0,0 +1,91 @@ +import { generateKeyPairSigner } from '@solana/kit'; +import * as zk from '@solana/zk-sdk/node'; +import test from 'ava'; +import { + getConfidentialTransferInstructionPlan, + getConfidentialWithdrawInstructionPlan, + getCreateConfidentialTransferAccountInstructionPlan, + type Token, +} from '../../../src'; +import { createDefaultSolanaClient } from '../../_setup'; + +test('it rejects create helper authority that does not match the owner ATA flow', async t => { + const [payer, owner, delegatedAuthority] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + const elgamalKeypair = new zk.ElGamalKeypair(); + const aesKey = new zk.AeKey(); + + await t.throwsAsync( + () => + getCreateConfidentialTransferAccountInstructionPlan({ + payer, + owner, + authority: delegatedAuthority, + mint: payer.address, + rpc: createDefaultSolanaClient().rpc, + zk, + elgamalKeypair, + aesKey, + }), + { + message: /authority must match owner/i, + }, + ); +}); + +test('it rejects instruction-data proof mode for confidential withdraw', async t => { + const [payer, owner] = await Promise.all([generateKeyPairSigner(), generateKeyPairSigner()]); + const elgamalKeypair = new zk.ElGamalKeypair(); + const aesKey = new zk.AeKey(); + + await t.throwsAsync( + () => + getConfidentialWithdrawInstructionPlan({ + payer, + rpc: createDefaultSolanaClient().rpc, + token: payer.address, + mint: owner.address, + tokenAccount: {} as Token, + authority: owner, + amount: 1n, + decimals: 0, + zk, + elgamalKeypair, + aesKey, + proofMode: 'instruction-data', + } as unknown as Parameters[0]), + { + message: /instruction-data proof mode is unsupported/i, + }, + ); +}); + +test('it rejects instruction-data proof mode for confidential transfer', async t => { + const [payer, owner] = await Promise.all([generateKeyPairSigner(), generateKeyPairSigner()]); + const sourceElgamalKeypair = new zk.ElGamalKeypair(); + const aesKey = new zk.AeKey(); + + await t.throwsAsync( + () => + getConfidentialTransferInstructionPlan({ + payer, + rpc: createDefaultSolanaClient().rpc, + sourceToken: payer.address, + mint: owner.address, + destinationToken: payer.address, + sourceTokenAccount: {} as Token, + authority: owner, + amount: 1n, + zk, + sourceElgamalKeypair, + aesKey, + proofMode: 'instruction-data', + } as unknown as Parameters[0]), + { + message: /instruction-data proof mode is unsupported/i, + }, + ); +}); 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/clients/js/test/extensions/confidentialTransfer/configureConfidentialTransferAccount.test.ts b/clients/js/test/extensions/confidentialTransfer/configureConfidentialTransferAccount.test.ts new file mode 100644 index 000000000..84d3c1231 --- /dev/null +++ b/clients/js/test/extensions/confidentialTransfer/configureConfidentialTransferAccount.test.ts @@ -0,0 +1,29 @@ +import { address, generateKeyPairSigner } from '@solana/kit'; +import test from 'ava'; +import { getConfigureConfidentialTransferAccountInstruction } from '../../../src'; + +const SYSVAR_INSTRUCTIONS_ADDRESS = address('Sysvar1nstructions1111111111111111111111111'); +const DECRYPTABLE_ZERO_BALANCE = new Uint8Array(36); + +test('it emits exactly 4 accounts in inline-proof mode', async t => { + const [token, mint, authority] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfigureConfidentialTransferAccountInstruction({ + token: token.address, + mint: mint.address, + authority, + decryptableZeroBalance: DECRYPTABLE_ZERO_BALANCE, + maximumPendingBalanceCreditCounter: 65_536n, + proofInstructionOffset: 1, + }); + + t.is(instruction.accounts.length, 4); + t.is(instruction.accounts[0].address, token.address); + t.is(instruction.accounts[1].address, mint.address); + t.is(instruction.accounts[2].address, SYSVAR_INSTRUCTIONS_ADDRESS); + t.is(instruction.accounts[3].address, authority.address); +}); diff --git a/clients/js/test/extensions/confidentialTransfer/configureConfidentialTransferAccountWithRegistry.test.ts b/clients/js/test/extensions/confidentialTransfer/configureConfidentialTransferAccountWithRegistry.test.ts new file mode 100644 index 000000000..336b23cea --- /dev/null +++ b/clients/js/test/extensions/confidentialTransfer/configureConfidentialTransferAccountWithRegistry.test.ts @@ -0,0 +1,87 @@ +import { address, generateKeyPairSigner, type AccountMeta } from '@solana/kit'; +import test from 'ava'; +import { + getConfigureConfidentialTransferAccountWithRegistryInstruction, + parseConfigureConfidentialTransferAccountWithRegistryInstruction, +} from '../../../src'; + +const SYSTEM_PROGRAM_ADDRESS = address('11111111111111111111111111111111'); + +test('it encodes the 27/14 discriminator pair', async t => { + const [token, mint, elgamalRegistry] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfigureConfidentialTransferAccountWithRegistryInstruction({ + token: token.address, + mint: mint.address, + elgamalRegistry: elgamalRegistry.address, + }); + + t.is(instruction.data[0], 27); + t.is(instruction.data[1], 14); + t.is(instruction.data.length, 2); +}); + +test('it emits a 3-account layout when payer and systemProgram are omitted', async t => { + const [token, mint, elgamalRegistry] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfigureConfidentialTransferAccountWithRegistryInstruction({ + token: token.address, + mint: mint.address, + elgamalRegistry: elgamalRegistry.address, + }); + const accounts = instruction.accounts as readonly AccountMeta[]; + + t.is(accounts.length, 3); + t.is(accounts[0].address, token.address); + t.is(accounts[1].address, mint.address); + t.is(accounts[2].address, elgamalRegistry.address); +}); + +test('it emits a 5-account layout when payer is provided', async t => { + const [token, mint, elgamalRegistry, payer] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfigureConfidentialTransferAccountWithRegistryInstruction({ + token: token.address, + mint: mint.address, + elgamalRegistry: elgamalRegistry.address, + payer, + systemProgram: SYSTEM_PROGRAM_ADDRESS, + }); + const accounts = instruction.accounts as readonly AccountMeta[]; + + t.is(accounts.length, 5); + t.is(accounts[3].address, payer.address); + t.is(accounts[4].address, SYSTEM_PROGRAM_ADDRESS); +}); + +test('it round-trips through parse', async t => { + const [token, mint, elgamalRegistry] = await Promise.all([ + generateKeyPairSigner(), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + const instruction = getConfigureConfidentialTransferAccountWithRegistryInstruction({ + token: token.address, + mint: mint.address, + elgamalRegistry: elgamalRegistry.address, + }); + const parsed = parseConfigureConfidentialTransferAccountWithRegistryInstruction(instruction); + + t.is(parsed.accounts.token.address, token.address); + t.is(parsed.accounts.mint.address, mint.address); + t.is(parsed.accounts.elgamalRegistry.address, elgamalRegistry.address); +}); diff --git a/clients/js/tsconfig.json b/clients/js/tsconfig.json index 1bc202aff..de75bed56 100644 --- a/clients/js/tsconfig.json +++ b/clients/js/tsconfig.json @@ -9,7 +9,7 @@ "inlineSources": false, "isolatedModules": true, "module": "ESNext", - "moduleResolution": "node", + "moduleResolution": "bundler", "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, diff --git a/interface/idl.json b/interface/idl.json index cf4a2a5c9..d6f7d2d35 100644 --- a/interface/idl.json +++ b/interface/idl.json @@ -3228,16 +3228,6 @@ "publicKey": "Sysvar1nstructions1111111111111111111111111" } }, - { - "kind": "instructionAccountNode", - "name": "record", - "isWritable": false, - "isSigner": false, - "isOptional": true, - "docs": [ - "(Optional) Record account if the accompanying proof is to be read from a record account." - ] - }, { "kind": "instructionAccountNode", "name": "authority", @@ -3341,6 +3331,112 @@ } ] }, + { + "kind": "instructionNode", + "name": "configureConfidentialTransferAccountWithRegistry", + "docs": [ + "Configures confidential transfers for a token account using an", + "ElGamal registry account that supplies the encryption pubkey.", + "", + "Unlike `ConfigureAccount`, this instruction does not require a", + "`VerifyPubkeyValidity` proof: the registry already attests to the", + "validity of the ElGamal public key.", + "", + "If the token account does not yet have room for the", + "`ConfidentialTransferAccount` extension, pass `payer` and", + "`systemProgram` so the instruction can fund the required reallocation." + ], + "optionalAccountStrategy": "omitted", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "token", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": ["The SPL Token account to configure."] + }, + { + "kind": "instructionAccountNode", + "name": "mint", + "isWritable": false, + "isSigner": false, + "isOptional": false, + "docs": ["The corresponding SPL Token mint."] + }, + { + "kind": "instructionAccountNode", + "name": "elgamalRegistry", + "isWritable": false, + "isSigner": false, + "isOptional": false, + "docs": ["The ElGamal registry account that provides the encryption pubkey for the owner."] + }, + { + "kind": "instructionAccountNode", + "name": "payer", + "isWritable": true, + "isSigner": true, + "isOptional": true, + "docs": [ + "(Optional) Payer that funds the rent required to reallocate the token", + "account when it does not yet have the `ConfidentialTransferAccount` extension." + ] + }, + { + "kind": "instructionAccountNode", + "name": "systemProgram", + "isWritable": false, + "isSigner": false, + "isOptional": true, + "docs": ["(Optional) System program, required when `payer` is provided."] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "defaultValueStrategy": "omitted", + "docs": [], + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 27 + } + }, + { + "kind": "instructionArgumentNode", + "name": "confidentialTransferDiscriminator", + "defaultValueStrategy": "omitted", + "docs": [], + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 14 + } + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + }, + { + "kind": "fieldDiscriminatorNode", + "name": "confidentialTransferDiscriminator", + "offset": 1 + } + ] + }, { "kind": "instructionNode", "name": "approveConfidentialTransferAccount", @@ -3703,7 +3799,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 +3987,7 @@ "", "Fails if the associated mint is extended as `NonTransferable`." ], - "optionalAccountStrategy": "programId", + "optionalAccountStrategy": "omitted", "accounts": [ { "kind": "instructionAccountNode", @@ -4002,6 +4098,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",