Skip to content

Commit aa9a35d

Browse files
Add Confidential Transfer Fee extension support, remove mintburn placeholder
1 parent 69b7c0d commit aa9a35d

File tree

5 files changed

+149
-3
lines changed

5 files changed

+149
-3
lines changed

apps/app/src/config/templates.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export const templates: Template[] = [
2727
'pausable',
2828
'permanentDelegate',
2929
'confidentialBalances',
30-
'confidentialMintBurn',
3130
],
3231
enabledExtensionKeys: [
3332
'extMetadata',

apps/app/src/features/token-creation/capabilities/registry.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ export const capabilityNodes: Record<CapabilityKey, ReactNode> = {
8282
),
8383
confidentialMintBurn: (
8484
<>
85-
<strong>Confidential mint/burn</strong>: Feature under audit enabling mint/burn with encrypted amounts.
86-
Amounts are not revealed to anyone but the token owner and an optional auditor.
85+
<strong>Confidential mint/burn</strong>: <em>(Coming soon)</em> Feature enabling mint/burn with encrypted
86+
amounts. Amounts are not revealed to anyone but the token owner and an optional auditor.
8787
</>
8888
),
8989
scaledUIAmount: (

packages/sdk/src/issuance/__tests__/index.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,78 @@ describe('Token', () => {
121121
});
122122
});
123123

124+
describe('withConfidentialTransferFee', () => {
125+
it('should throw error if ConfidentialTransferMint is not enabled', () => {
126+
expect(() => {
127+
token.withConfidentialTransferFee({
128+
authority: TEST_AUTHORITY,
129+
withdrawWithheldAuthorityElGamalPubkey: TEST_AUTHORITY,
130+
});
131+
}).toThrow('ConfidentialTransferMint extension must be enabled before adding ConfidentialTransferFee');
132+
});
133+
134+
it('should throw error if TransferFeeConfig is not enabled', () => {
135+
token.withConfidentialBalances(TEST_AUTHORITY);
136+
expect(() => {
137+
token.withConfidentialTransferFee({
138+
authority: TEST_AUTHORITY,
139+
withdrawWithheldAuthorityElGamalPubkey: TEST_AUTHORITY,
140+
});
141+
}).toThrow('TransferFeeConfig extension must be enabled before adding ConfidentialTransferFee');
142+
});
143+
144+
it('should add confidential transfer fee config when both required extensions are present', () => {
145+
const elGamalPubkey = generateMockAddress() as Address;
146+
const result = token
147+
.withConfidentialBalances(TEST_AUTHORITY)
148+
.withTransferFee({
149+
authority: TEST_AUTHORITY,
150+
withdrawAuthority: TEST_AUTHORITY,
151+
feeBasisPoints: 100,
152+
maximumFee: 1000n,
153+
})
154+
.withConfidentialTransferFee({
155+
authority: TEST_AUTHORITY,
156+
withdrawWithheldAuthorityElGamalPubkey: elGamalPubkey,
157+
});
158+
159+
expect(result).toBe(token);
160+
expect(token.getExtensions()).toHaveLength(2); // ConfidentialTransferMint + TransferFeeConfig
161+
});
162+
163+
it('should include confidential transfer fee instruction in buildInstructions', async () => {
164+
const elGamalPubkey = generateMockAddress() as Address;
165+
token
166+
.withConfidentialBalances(TEST_AUTHORITY)
167+
.withTransferFee({
168+
authority: TEST_AUTHORITY,
169+
withdrawAuthority: TEST_AUTHORITY,
170+
feeBasisPoints: 100,
171+
maximumFee: 1000n,
172+
})
173+
.withConfidentialTransferFee({
174+
authority: TEST_AUTHORITY,
175+
withdrawWithheldAuthorityElGamalPubkey: elGamalPubkey,
176+
});
177+
178+
const instructions = await token.buildInstructions({
179+
rpc: mockRpc,
180+
decimals: 6,
181+
mintAuthority: TEST_AUTHORITY,
182+
mint: mockMint,
183+
feePayer: mockFeePayer,
184+
});
185+
186+
// Should have: create + pre-init (confidential transfer mint + confidential transfer fee) + init
187+
expect(instructions.length).toBeGreaterThan(2);
188+
// Check that one of the pre-init instructions is for confidential transfer fee
189+
const hasConfidentialTransferFeeInstruction = instructions.some(
190+
(inst: any) => inst.programAddress === TOKEN_2022_PROGRAM_ADDRESS && inst.data,
191+
);
192+
expect(hasConfidentialTransferFeeInstruction).toBe(true);
193+
});
194+
});
195+
124196
describe('method chaining', () => {
125197
it('should allow chaining multiple extensions', () => {
126198
const additionalMetadata = createTestAdditionalMetadata();

packages/sdk/src/issuance/create-mint.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@ import {
1919
getPreInitializeInstructionsForMintExtensions,
2020
TOKEN_2022_PROGRAM_ADDRESS,
2121
getInitializeTokenMetadataInstruction,
22+
getInitializeConfidentialTransferFeeInstruction,
2223
} from '@solana-program/token-2022';
2324
import { createUpdateFieldInstruction } from './create-update-field-instruction';
2425

2526
export class Token {
2627
private extensions: Extension[] = [];
28+
private confidentialTransferFeeConfig?: {
29+
authority: Address;
30+
withdrawWithheldAuthorityElGamalPubkey: Address;
31+
};
2732

2833
getExtensions(): Extension[] {
2934
return this.extensions;
@@ -200,6 +205,33 @@ export class Token {
200205
return this;
201206
}
202207

208+
/**
209+
* Adds the Confidential Transfer Fee extension to the token.
210+
* Enables confidential transfers with fees. Requires both ConfidentialTransferMint
211+
* and TransferFee extensions to be enabled.
212+
*
213+
* @param config - Confidential transfer fee configuration
214+
* @param config.authority - Optional authority to set the withdraw withheld authority ElGamal key
215+
* @param config.withdrawWithheldAuthorityElGamalPubkey - ElGamal public key for encrypted withheld fees
216+
*/
217+
withConfidentialTransferFee(config: {
218+
authority: Address;
219+
withdrawWithheldAuthorityElGamalPubkey: Address;
220+
}): Token {
221+
// Check that ConfidentialTransferMint is enabled
222+
if (!this.extensions.some(ext => ext.__kind === 'ConfidentialTransferMint')) {
223+
throw new Error(
224+
'ConfidentialTransferMint extension must be enabled before adding ConfidentialTransferFee',
225+
);
226+
}
227+
// Check that TransferFeeConfig is enabled
228+
if (!this.extensions.some(ext => ext.__kind === 'TransferFeeConfig')) {
229+
throw new Error('TransferFeeConfig extension must be enabled before adding ConfidentialTransferFee');
230+
}
231+
this.confidentialTransferFeeConfig = config;
232+
return this;
233+
}
234+
203235
async buildInstructions({
204236
rpc,
205237
decimals,
@@ -237,6 +269,24 @@ export class Token {
237269
getPreInitializeInstructionsForMintExtensions(mint.address, [ext]),
238270
);
239271

272+
// Add ConfidentialTransferFee initialization if configured
273+
if (this.confidentialTransferFeeConfig) {
274+
preInitializeInstructions.push(
275+
getInitializeConfidentialTransferFeeInstruction(
276+
{
277+
mint: mint.address,
278+
authority: some(this.confidentialTransferFeeConfig.authority),
279+
withdrawWithheldAuthorityElGamalPubkey: some(
280+
this.confidentialTransferFeeConfig.withdrawWithheldAuthorityElGamalPubkey,
281+
),
282+
},
283+
{
284+
programAddress: TOKEN_2022_PROGRAM_ADDRESS,
285+
},
286+
),
287+
);
288+
}
289+
240290
// TODO: Add other post-initialize instructions as needed like for transfer hooks
241291
if (
242292
this.extensions.some(ext => ext.__kind === 'TokenMetadata') &&

packages/sdk/src/templates/custom-token.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const createCustomTokenInitTransaction = async (
5454
enableScaledUiAmount?: boolean;
5555
enableSrfc37?: boolean;
5656
enableTransferFee?: boolean;
57+
enableConfidentialTransferFee?: boolean;
5758
enableInterestBearing?: boolean;
5859
enableNonTransferable?: boolean;
5960
enableTransferHook?: boolean;
@@ -85,6 +86,10 @@ export const createCustomTokenInitTransaction = async (
8586
transferFeeBasisPoints?: number;
8687
transferFeeMaximum?: bigint;
8788

89+
// Confidential Transfer Fee configuration
90+
confidentialTransferFeeAuthority?: Address;
91+
withdrawWithheldAuthorityElGamalPubkey?: Address;
92+
8893
// Interest Bearing configuration
8994
interestBearingAuthority?: Address;
9095
interestRate?: number;
@@ -200,6 +205,26 @@ export const createCustomTokenInitTransaction = async (
200205
});
201206
}
202207

208+
// Add Confidential Transfer Fee extension
209+
if (options?.enableConfidentialTransferFee) {
210+
if (!options?.enableConfidentialBalances) {
211+
throw new Error(
212+
'enableConfidentialBalances must be enabled when enableConfidentialTransferFee is enabled',
213+
);
214+
}
215+
if (!options?.enableTransferFee) {
216+
throw new Error('enableTransferFee must be enabled when enableConfidentialTransferFee is enabled');
217+
}
218+
if (!options.withdrawWithheldAuthorityElGamalPubkey) {
219+
throw new Error('withdrawWithheldAuthorityElGamalPubkey is required when enableConfidentialTransferFee is enabled');
220+
}
221+
const confidentialTransferFeeAuthority = options.confidentialTransferFeeAuthority || mintAuthorityAddress;
222+
tokenBuilder = tokenBuilder.withConfidentialTransferFee({
223+
authority: confidentialTransferFeeAuthority,
224+
withdrawWithheldAuthorityElGamalPubkey: options.withdrawWithheldAuthorityElGamalPubkey,
225+
});
226+
}
227+
203228
// Add Interest Bearing extension
204229
if (options?.enableInterestBearing) {
205230
const rate = options.interestRate ?? 0;

0 commit comments

Comments
 (0)