diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efffc7a9a2c..d598d08b40d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -777,6 +777,9 @@ importers: buffer: specifier: ^6.0.3 version: 6.0.3 + solana-zk-token-sdk-experimental: + specifier: ^0.1.1 + version: 0.1.1 devDependencies: '@solana/codecs-strings': specifier: 2.0.0-preview.2 @@ -7608,6 +7611,10 @@ packages: - utf-8-validate dev: true + /solana-zk-token-sdk-experimental@0.1.1: + resolution: {integrity: sha512-pEul58buHN4lNpemHRlX5ZOSqImh3XOwWcwg+QyhQC9ZQ3pgY5Lin9NoW7psdsOfxIOyjPWiotkAq/A3xuCfvg==} + dev: false + /source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} dependencies: diff --git a/token/js/package.json b/token/js/package.json index 98c3d856aa0..3b60a132124 100644 --- a/token/js/package.json +++ b/token/js/package.json @@ -58,7 +58,8 @@ "@solana/buffer-layout-utils": "^0.2.0", "@solana/spl-token-group": "^0.0.2", "@solana/spl-token-metadata": "^0.1.2", - "buffer": "^6.0.3" + "buffer": "^6.0.3", + "solana-zk-token-sdk-experimental": "^0.1.1" }, "devDependencies": { "@solana/codecs-strings": "2.0.0-preview.2", diff --git a/token/js/src/extensions/confidentialTransfer/elgamal.ts b/token/js/src/extensions/confidentialTransfer/elgamal.ts new file mode 100644 index 00000000000..4ecb6c00dbe --- /dev/null +++ b/token/js/src/extensions/confidentialTransfer/elgamal.ts @@ -0,0 +1,22 @@ +import { blob, Layout } from '@solana/buffer-layout'; +import { encodeDecode } from '@solana/buffer-layout-utils'; +import { PodElGamalPubkey } from 'solana-zk-token-sdk-experimental'; + +export const elgamalPublicKey = (property?: string): Layout => { + const layout = blob(32, property); + const { encode, decode } = encodeDecode(layout); + + const elgamalPublicKeyLayout = layout as Layout as Layout; + + elgamalPublicKeyLayout.decode = (buffer: Buffer, offset: number) => { + const src = decode(buffer, offset); + return new PodElGamalPubkey(src); + }; + + elgamalPublicKeyLayout.encode = (elgamalPublicKey: PodElGamalPubkey, buffer: Buffer, offset: number) => { + const src = elgamalPublicKey.toBytes(); + return encode(src, buffer, offset); + }; + + return elgamalPublicKeyLayout; +}; diff --git a/token/js/src/extensions/confidentialTransfer/index.ts b/token/js/src/extensions/confidentialTransfer/index.ts new file mode 100644 index 00000000000..d1cedccd563 --- /dev/null +++ b/token/js/src/extensions/confidentialTransfer/index.ts @@ -0,0 +1,2 @@ +export * from './state.js'; +export * from './instructions.js'; diff --git a/token/js/src/extensions/confidentialTransfer/instructions.ts b/token/js/src/extensions/confidentialTransfer/instructions.ts new file mode 100644 index 00000000000..bb586cf52d4 --- /dev/null +++ b/token/js/src/extensions/confidentialTransfer/instructions.ts @@ -0,0 +1,56 @@ +import { struct, u8 } from '@solana/buffer-layout'; +import { bool, publicKey } from '@solana/buffer-layout-utils'; +import { PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../../constants.js'; +import { TokenUnsupportedInstructionError } from '../../errors.js'; +import { TokenInstruction } from '../../instructions/types.js'; +import { PodElGamalPubkey } from 'solana-zk-token-sdk-experimental'; +import { elgamalPublicKey } from './elgamal.js'; + +export enum ConfidentialTransferInstruction { + InitializeMint = 0, +} + +export interface InitializeMintData { + instruction: TokenInstruction.ConfidentialTransferExtension; + confidentialTransferInstruction: ConfidentialTransferInstruction.InitializeMint; + confidentialTransferMintAuthority: PublicKey | null; + autoApproveNewAccounts: boolean; + auditorElGamalPubkey: PodElGamalPubkey | null; +} + +export const initializeMintData = struct([ + u8('instruction'), + u8('confidentialTransferInstruction'), + publicKey('confidentialTransferMintAuthority'), + bool('autoApproveNewAccounts'), + elgamalPublicKey('auditorElGamalPubkey'), +]); + +export function createConfidentialTransferInitializeMintInstruction( + mint: PublicKey, + confidentialTransferMintAuthority: PublicKey | null, + autoApproveNewAccounts: boolean, + auditorElGamalPubkey: PodElGamalPubkey | null, + programId = TOKEN_2022_PROGRAM_ID +): TransactionInstruction { + if (!programSupportsExtensions(programId)) { + throw new TokenUnsupportedInstructionError(); + } + const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; + + const data = Buffer.alloc(initializeMintData.span); + + initializeMintData.encode( + { + instruction: TokenInstruction.ConfidentialTransferExtension, + confidentialTransferInstruction: ConfidentialTransferInstruction.InitializeMint, + confidentialTransferMintAuthority: confidentialTransferMintAuthority ?? PublicKey.default, + autoApproveNewAccounts: autoApproveNewAccounts, + auditorElGamalPubkey: auditorElGamalPubkey ?? PodElGamalPubkey.default(), + }, + data + ); + + return new TransactionInstruction({ keys, programId, data }); +} diff --git a/token/js/src/extensions/confidentialTransfer/state.ts b/token/js/src/extensions/confidentialTransfer/state.ts new file mode 100644 index 00000000000..e46f61b5fb5 --- /dev/null +++ b/token/js/src/extensions/confidentialTransfer/state.ts @@ -0,0 +1,30 @@ +import { struct } from '@solana/buffer-layout'; +import { publicKey, bool } from '@solana/buffer-layout-utils'; +import type { PublicKey } from '@solana/web3.js'; +import type { Mint } from '../../state/mint.js'; +import { ExtensionType, getExtensionData } from '../extensionType.js'; +import { PodElGamalPubkey } from 'solana-zk-token-sdk-experimental'; +import { elgamalPublicKey } from './elgamal.js'; + +export interface ConfidentialTransferMint { + confidentialTransferMintAuthority: PublicKey; + autoApproveNewAccounts: boolean; + auditorElGamalPubkey: PodElGamalPubkey; +} + +export const ConfidentialTransferMintLayout = struct([ + publicKey('confidentialTransferMintAuthority'), + bool('autoApproveNewAccounts'), + elgamalPublicKey('auditorElGamalPubkey'), +]); + +export const CONFIDENTIAL_TRANSFER_MINT_SIZE = ConfidentialTransferMintLayout.span; + +export function getConfidentialTransferMint(mint: Mint): ConfidentialTransferMint | null { + const extensionData = getExtensionData(ExtensionType.ConfidentialTransferMint, mint.tlvData); + if (extensionData !== null) { + return ConfidentialTransferMintLayout.decode(extensionData); + } else { + return null; + } +} diff --git a/token/js/src/extensions/extensionType.ts b/token/js/src/extensions/extensionType.ts index 2eadd1c973e..36b3859d8a6 100644 --- a/token/js/src/extensions/extensionType.ts +++ b/token/js/src/extensions/extensionType.ts @@ -20,6 +20,7 @@ import { PERMANENT_DELEGATE_SIZE } from './permanentDelegate.js'; import { TRANSFER_FEE_AMOUNT_SIZE, TRANSFER_FEE_CONFIG_SIZE } from './transferFee/index.js'; import { TRANSFER_HOOK_ACCOUNT_SIZE, TRANSFER_HOOK_SIZE } from './transferHook/index.js'; import { TOKEN_2022_PROGRAM_ID } from '../constants.js'; +import { CONFIDENTIAL_TRANSFER_MINT_SIZE } from './confidentialTransfer/state.js'; // Sequence from https://github.com/solana-labs/solana-program-library/blob/master/token/program-2022/src/extension/mod.rs#L903 export enum ExtensionType { @@ -78,7 +79,7 @@ export function getTypeLen(e: ExtensionType): number { case ExtensionType.MintCloseAuthority: return MINT_CLOSE_AUTHORITY_SIZE; case ExtensionType.ConfidentialTransferMint: - return 65; + return CONFIDENTIAL_TRANSFER_MINT_SIZE; case ExtensionType.ConfidentialTransferAccount: return 295; case ExtensionType.CpiGuard: diff --git a/token/js/test/e2e-2022/confidentialTransfer.test.ts b/token/js/test/e2e-2022/confidentialTransfer.test.ts new file mode 100644 index 00000000000..8544c170efb --- /dev/null +++ b/token/js/test/e2e-2022/confidentialTransfer.test.ts @@ -0,0 +1,89 @@ +import { expect } from 'chai'; +import type { Connection, Signer } from '@solana/web3.js'; +import { PublicKey } from '@solana/web3.js'; +import { Keypair, SystemProgram, Transaction, sendAndConfirmTransaction } from '@solana/web3.js'; +import { ElGamalKeypair, PodElGamalPubkey } from 'solana-zk-token-sdk-experimental'; +import { ExtensionType, createInitializeMintInstruction, getMint, getMintLen } from '../../src'; +import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; + +import { + createConfidentialTransferInitializeMintInstruction, + getConfidentialTransferMint, +} from '../../src/extensions/confidentialTransfer/index'; + +const TEST_TOKEN_DECIMALS = 2; +const MINT_EXTENSIONS = [ExtensionType.ConfidentialTransferMint]; + +describe('confidentialTransfer', () => { + let connection: Connection; + let payer: Signer; + let mint: PublicKey; + let mintAuthority: Keypair; + before(async () => { + connection = await getConnection(); + payer = await newAccountWithLamports(connection, 1000000000); + }); + + async function setupConfidentialTransferMint( + confidentialTransferMintAuthority: PublicKey | null, + autoApproveNewAccounts: boolean, + auditorPubkey: PodElGamalPubkey | null + ) { + const mintKeypair = Keypair.generate(); + mint = mintKeypair.publicKey; + mintAuthority = Keypair.generate(); + const mintLen = getMintLen(MINT_EXTENSIONS); + + const mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen); + const mintTransaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mint, + space: mintLen, + lamports: mintLamports, + programId: TEST_PROGRAM_ID, + }), + createConfidentialTransferInitializeMintInstruction( + mint, + confidentialTransferMintAuthority, + autoApproveNewAccounts, + auditorPubkey + ), + createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, mintAuthority.publicKey, null, TEST_PROGRAM_ID) + ); + + await sendAndConfirmTransaction(connection, mintTransaction, [payer, mintKeypair], undefined); + } + + describe('with authorities and auto approve', () => { + let confidentialTransferMintAuthority: Keypair; + let autoApproveNewAccounts: boolean; + let auditorKeypair: ElGamalKeypair; + let auditorPubkey: PodElGamalPubkey; + beforeEach(async () => { + confidentialTransferMintAuthority = Keypair.generate(); + autoApproveNewAccounts = true; + auditorKeypair = ElGamalKeypair.new_rand(); + auditorPubkey = PodElGamalPubkey.encoded(auditorKeypair.pubkey_owned()); + + await setupConfidentialTransferMint( + confidentialTransferMintAuthority.publicKey, + autoApproveNewAccounts, + auditorPubkey + ); + }); + + it('initializes', async () => { + const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); + const confidentialTransferMint = getConfidentialTransferMint(mintInfo); + expect(confidentialTransferMint).to.not.be.null; + if (confidentialTransferMint !== null) { + expect(confidentialTransferMint.confidentialTransferMintAuthority).to.eql( + confidentialTransferMintAuthority.publicKey + ); + expect(confidentialTransferMint.autoApproveNewAccounts).to.eql(autoApproveNewAccounts); + expect(confidentialTransferMint.auditorElGamalPubkey.equals(auditorPubkey)); // TODO: equals? + } + }); + }); +});