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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions apps/app/src/config/templates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ export const capabilityNodes: Record<CapabilityKey, ReactNode> = {
),
confidentialMintBurn: (
<>
<strong>Confidential mint/burn</strong>: Feature under audit enabling mint/burn with encrypted amounts.
Amounts are not revealed to anyone but the token owner and an optional auditor.
<strong>Confidential mint/burn</strong>: <em>(Coming soon)</em> Feature enabling mint/burn with encrypted
amounts. Amounts are not revealed to anyone but the token owner and an optional auditor.
</>
),
scaledUIAmount: (
Expand Down
72 changes: 72 additions & 0 deletions packages/sdk/src/issuance/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
48 changes: 48 additions & 0 deletions packages/sdk/src/issuance/create-mint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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') &&
Expand Down
25 changes: 25 additions & 0 deletions packages/sdk/src/templates/custom-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const createCustomTokenInitTransaction = async (
enableScaledUiAmount?: boolean;
enableSrfc37?: boolean;
enableTransferFee?: boolean;
enableConfidentialTransferFee?: boolean;
enableInterestBearing?: boolean;
enableNonTransferable?: boolean;
enableTransferHook?: boolean;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down