diff --git a/apps/app/src/config/templates.tsx b/apps/app/src/config/templates.tsx index 368d791..409e08c 100644 --- a/apps/app/src/config/templates.tsx +++ b/apps/app/src/config/templates.tsx @@ -21,14 +21,7 @@ export const templates: Template[] = [ description: 'Create a regulatory-compliant stablecoin with transfer restrictions and metadata management.', colorClass: 'bg-indigo-100', iconColorClass: 'text-indigo-600', - coreCapabilityKeys: [ - 'metadata', - 'accessControls', - 'pausable', - 'permanentDelegate', - 'confidentialBalances', - 'confidentialMintBurn', - ], + coreCapabilityKeys: ['metadata', 'accessControls', 'pausable', 'permanentDelegate', 'confidentialBalances'], enabledExtensionKeys: [ 'extMetadata', 'extPausable', diff --git a/apps/app/src/features/token-creation/capabilities/registry.tsx b/apps/app/src/features/token-creation/capabilities/registry.tsx index eaf9d18..636a493 100644 --- a/apps/app/src/features/token-creation/capabilities/registry.tsx +++ b/apps/app/src/features/token-creation/capabilities/registry.tsx @@ -82,8 +82,8 @@ export const capabilityNodes: Record = { ), confidentialMintBurn: ( <> - Confidential mint/burn: Feature under audit enabling mint/burn with encrypted amounts. - Amounts are not revealed to anyone but the token owner and an optional auditor. + Confidential mint/burn: (Coming soon) Feature enabling mint/burn with encrypted + amounts. Amounts are not revealed to anyone but the token owner and an optional auditor. ), scaledUIAmount: ( diff --git a/packages/sdk/src/issuance/__tests__/index.test.ts b/packages/sdk/src/issuance/__tests__/index.test.ts index 24227ab..7a265c5 100644 --- a/packages/sdk/src/issuance/__tests__/index.test.ts +++ b/packages/sdk/src/issuance/__tests__/index.test.ts @@ -121,6 +121,78 @@ describe('Token', () => { }); }); + describe('withConfidentialTransferFee', () => { + it('should throw error if ConfidentialTransferMint is not enabled', () => { + expect(() => { + token.withConfidentialTransferFee({ + authority: TEST_AUTHORITY, + withdrawWithheldAuthorityElGamalPubkey: TEST_AUTHORITY, + }); + }).toThrow('ConfidentialTransferMint extension must be enabled before adding ConfidentialTransferFee'); + }); + + it('should throw error if TransferFeeConfig is not enabled', () => { + token.withConfidentialBalances(TEST_AUTHORITY); + expect(() => { + token.withConfidentialTransferFee({ + authority: TEST_AUTHORITY, + withdrawWithheldAuthorityElGamalPubkey: TEST_AUTHORITY, + }); + }).toThrow('TransferFeeConfig extension must be enabled before adding ConfidentialTransferFee'); + }); + + it('should add confidential transfer fee config when both required extensions are present', () => { + const elGamalPubkey = generateMockAddress() as Address; + const result = token + .withConfidentialBalances(TEST_AUTHORITY) + .withTransferFee({ + authority: TEST_AUTHORITY, + withdrawAuthority: TEST_AUTHORITY, + feeBasisPoints: 100, + maximumFee: 1000n, + }) + .withConfidentialTransferFee({ + authority: TEST_AUTHORITY, + withdrawWithheldAuthorityElGamalPubkey: elGamalPubkey, + }); + + expect(result).toBe(token); + expect(token.getExtensions()).toHaveLength(2); // ConfidentialTransferMint + TransferFeeConfig + }); + + it('should include confidential transfer fee instruction in buildInstructions', async () => { + const elGamalPubkey = generateMockAddress() as Address; + token + .withConfidentialBalances(TEST_AUTHORITY) + .withTransferFee({ + authority: TEST_AUTHORITY, + withdrawAuthority: TEST_AUTHORITY, + feeBasisPoints: 100, + maximumFee: 1000n, + }) + .withConfidentialTransferFee({ + authority: TEST_AUTHORITY, + withdrawWithheldAuthorityElGamalPubkey: elGamalPubkey, + }); + + const instructions = await token.buildInstructions({ + rpc: mockRpc, + decimals: 6, + mintAuthority: TEST_AUTHORITY, + mint: mockMint, + feePayer: mockFeePayer, + }); + + // Should have: create + pre-init (confidential transfer mint + confidential transfer fee) + init + expect(instructions.length).toBeGreaterThan(2); + // Check that one of the pre-init instructions is for confidential transfer fee + const hasConfidentialTransferFeeInstruction = instructions.some( + (inst: any) => inst.programAddress === TOKEN_2022_PROGRAM_ADDRESS && inst.data, + ); + expect(hasConfidentialTransferFeeInstruction).toBe(true); + }); + }); + describe('method chaining', () => { it('should allow chaining multiple extensions', () => { const additionalMetadata = createTestAdditionalMetadata(); diff --git a/packages/sdk/src/issuance/create-mint.ts b/packages/sdk/src/issuance/create-mint.ts index 545fb82..ee2c0e0 100644 --- a/packages/sdk/src/issuance/create-mint.ts +++ b/packages/sdk/src/issuance/create-mint.ts @@ -19,11 +19,16 @@ import { getPreInitializeInstructionsForMintExtensions, TOKEN_2022_PROGRAM_ADDRESS, getInitializeTokenMetadataInstruction, + getInitializeConfidentialTransferFeeInstruction, } from '@solana-program/token-2022'; import { createUpdateFieldInstruction } from './create-update-field-instruction'; export class Token { private extensions: Extension[] = []; + private confidentialTransferFeeConfig?: { + authority: Address; + withdrawWithheldAuthorityElGamalPubkey: Address; + }; getExtensions(): Extension[] { return this.extensions; @@ -200,6 +205,31 @@ export class Token { return this; } + /** + * Adds the Confidential Transfer Fee extension to the token. + * Enables confidential transfers with fees. Requires both ConfidentialTransferMint + * and TransferFee extensions to be enabled. + * + * @param config - Confidential transfer fee configuration + * @param config.authority - Authority to set the withdraw withheld authority ElGamal key + * @param config.withdrawWithheldAuthorityElGamalPubkey - ElGamal public key for encrypted withheld fees + */ + withConfidentialTransferFee(config: { + authority: Address; + withdrawWithheldAuthorityElGamalPubkey: Address; + }): Token { + // Check that ConfidentialTransferMint is enabled + if (!this.extensions.some(ext => ext.__kind === 'ConfidentialTransferMint')) { + throw new Error('ConfidentialTransferMint extension must be enabled before adding ConfidentialTransferFee'); + } + // Check that TransferFeeConfig is enabled + if (!this.extensions.some(ext => ext.__kind === 'TransferFeeConfig')) { + throw new Error('TransferFeeConfig extension must be enabled before adding ConfidentialTransferFee'); + } + this.confidentialTransferFeeConfig = config; + return this; + } + async buildInstructions({ rpc, decimals, @@ -237,6 +267,24 @@ export class Token { getPreInitializeInstructionsForMintExtensions(mint.address, [ext]), ); + // Add ConfidentialTransferFee initialization if configured + if (this.confidentialTransferFeeConfig) { + preInitializeInstructions.push( + getInitializeConfidentialTransferFeeInstruction( + { + mint: mint.address, + authority: some(this.confidentialTransferFeeConfig.authority), + withdrawWithheldAuthorityElGamalPubkey: some( + this.confidentialTransferFeeConfig.withdrawWithheldAuthorityElGamalPubkey, + ), + }, + { + programAddress: TOKEN_2022_PROGRAM_ADDRESS, + }, + ), + ); + } + // TODO: Add other post-initialize instructions as needed like for transfer hooks if ( this.extensions.some(ext => ext.__kind === 'TokenMetadata') && diff --git a/packages/sdk/src/templates/custom-token.ts b/packages/sdk/src/templates/custom-token.ts index 4d35c2d..3ef0cfe 100644 --- a/packages/sdk/src/templates/custom-token.ts +++ b/packages/sdk/src/templates/custom-token.ts @@ -54,6 +54,7 @@ export const createCustomTokenInitTransaction = async ( enableScaledUiAmount?: boolean; enableSrfc37?: boolean; enableTransferFee?: boolean; + enableConfidentialTransferFee?: boolean; enableInterestBearing?: boolean; enableNonTransferable?: boolean; enableTransferHook?: boolean; @@ -85,6 +86,10 @@ export const createCustomTokenInitTransaction = async ( transferFeeBasisPoints?: number; transferFeeMaximum?: bigint; + // Confidential Transfer Fee configuration + confidentialTransferFeeAuthority?: Address; + withdrawWithheldAuthorityElGamalPubkey?: Address; + // Interest Bearing configuration interestBearingAuthority?: Address; interestRate?: number; @@ -200,6 +205,26 @@ export const createCustomTokenInitTransaction = async ( }); } + // Add Confidential Transfer Fee extension + if (options?.enableConfidentialTransferFee) { + if (!options?.enableConfidentialBalances) { + throw new Error('enableConfidentialBalances must be enabled when enableConfidentialTransferFee is enabled'); + } + if (!options?.enableTransferFee) { + throw new Error('enableTransferFee must be enabled when enableConfidentialTransferFee is enabled'); + } + if (!options.withdrawWithheldAuthorityElGamalPubkey) { + throw new Error( + 'withdrawWithheldAuthorityElGamalPubkey is required when enableConfidentialTransferFee is enabled', + ); + } + const confidentialTransferFeeAuthority = options.confidentialTransferFeeAuthority || mintAuthorityAddress; + tokenBuilder = tokenBuilder.withConfidentialTransferFee({ + authority: confidentialTransferFeeAuthority, + withdrawWithheldAuthorityElGamalPubkey: options.withdrawWithheldAuthorityElGamalPubkey, + }); + } + // Add Interest Bearing extension if (options?.enableInterestBearing) { const rate = options.interestRate ?? 0;