Skip to content

Commit c863ba0

Browse files
Copilotbhavidhingra
authored andcommitted
feat(sdk-coin-sol): add ConfidentialMintBuilder for proof-account tx group
TICKET: CHALO-1090
1 parent e85f12e commit c863ba0

9 files changed

Lines changed: 1326 additions & 8 deletions

File tree

modules/sdk-coin-sol/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"@bitgo/sdk-lib-mpc": "^10.15.0",
6363
"@bitgo/statics": "^59.0.0",
6464
"@bitgo/wasm-solana": "^2.6.0",
65+
"@solana/buffer-layout": "^4.0.1",
6566
"@solana/spl-stake-pool": "1.1.8",
6667
"@solana/spl-token": "0.4.9",
6768
"@solana/web3.js": "1.92.1",
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
2+
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
3+
import { Transaction } from './transaction';
4+
import { TransactionBuilder } from './transactionBuilder';
5+
import { INSTRUCTIONS_SYSVAR_ADDRESS, InstructionBuilderTypes, ZK_ELGAMAL_PROOF_PROGRAM_ID } from './constants';
6+
import {
7+
ConfidentialMint,
8+
ConfigureConfidentialTransferAccount,
9+
CreateRecordAccount,
10+
WriteRecordData,
11+
VerifyEqualityProof,
12+
VerifyValidityProof,
13+
VerifyRangeProof,
14+
CloseRecordAccount,
15+
CloseContextState,
16+
InstructionParams,
17+
} from './iface';
18+
import { validateAddress } from './utils';
19+
import assert from 'assert';
20+
21+
/**
22+
* Parameters for the confidential mint proof-account group.
23+
*/
24+
export interface ConfidentialMintParams {
25+
/** Token account receiving the confidential mint (must be Token-2022). */
26+
tokenAddress: string;
27+
/** Mint address (must be Token-2022 with ConfidentialTransferMint configured). */
28+
mintAddress: string;
29+
/** Mint authority that must sign the confidentialMint instruction. */
30+
authorityAddress: string;
31+
/** Payer / fee payer for creating proof accounts. */
32+
payerAddress: string;
33+
/** Address of the reusable record account for the range proof. */
34+
rangeRecordAddress: string;
35+
/** Address of the one-shot context state account for the equality proof. */
36+
equalityContextStateAddress: string;
37+
/** Address of the one-shot context state account for the ciphertext validity proof. */
38+
validityContextStateAddress: string;
39+
/** Authority that owns the context state accounts (usually the payer). */
40+
contextStateAuthorityAddress: string;
41+
/** Authority that owns the reusable record account. */
42+
recordAccountOwnerAddress?: string;
43+
/** Lamports for rent-exempt record account. If omitted, calculated from space. */
44+
recordAccountLamports?: number;
45+
/** 36-byte hex decryptable ciphertext of the new supply. */
46+
newDecryptableSupply: string;
47+
/** 64-byte hex low half of the auditor ciphertext. */
48+
mintAmountAuditorCiphertextLo: string;
49+
/** 64-byte hex high half of the auditor ciphertext. */
50+
mintAmountAuditorCiphertextHi: string;
51+
/** Hex data for the record account write (typically the 1000B BatchedRangeProofU128). */
52+
rangeRecordData: string;
53+
/** Optional second chunk of record data for 1-2 writes. */
54+
rangeRecordDataPart2?: string;
55+
/** Instruction offset to the equality proof verify instruction (relative to confidentialMint). */
56+
equalityProofInstructionOffset?: number;
57+
/** Instruction offset to the ciphertext validity proof verify instruction. */
58+
ciphertextValidityProofInstructionOffset?: number;
59+
/** Instruction offset to the range proof verify instruction. */
60+
rangeProofInstructionOffset?: number;
61+
/** Optional close the range record account after mint. */
62+
closeRecordAccount?: boolean;
63+
/** Optional close the equality context state account after mint. */
64+
closeEqualityContextState?: boolean;
65+
/** Optional close the validity context state account after mint. */
66+
closeValidityContextState?: boolean;
67+
}
68+
69+
/**
70+
* Parameters for configuring a confidential transfer token account.
71+
*/
72+
export interface ConfigureConfidentialTransferAccountParams {
73+
/** Token account to configure. */
74+
tokenAddress: string;
75+
/** Mint address. */
76+
mintAddress: string;
77+
/** Account owner / authority that must sign. */
78+
authorityAddress: string;
79+
/** Optional instructions sysvar or context state address for proof verification. */
80+
instructionsSysvarOrContextStateAddress?: string;
81+
/** 36-byte hex decryptable zero balance ciphertext. */
82+
decryptableZeroBalance: string;
83+
/** Maximum pending balance credit counter as a decimal string. */
84+
maximumPendingBalanceCreditCounter: string;
85+
/** Offset to the proof instruction (usually 0 when proof is in same tx). */
86+
proofInstructionOffset?: number;
87+
}
88+
89+
/**
90+
* Builder for Solana Token-2022 confidential mint transactions using the proof-account approach.
91+
*
92+
* This builder groups the proof-account tx sequence as one logical transaction:
93+
* create-record → write(1-2) → verify equality → verify validity → verify range → confidentialMint
94+
* → optional close record/context-state accounts.
95+
*
96+
* The actual zero-knowledge proofs are generated externally (e.g. KMS worker); the SDK only
97+
* assembles instruction data and account metadata.
98+
*/
99+
export class ConfidentialMintBuilder extends TransactionBuilder {
100+
private _mintParams?: ConfidentialMintParams;
101+
private _configureParams?: ConfigureConfidentialTransferAccountParams;
102+
103+
constructor(_coinConfig: Readonly<CoinConfig>) {
104+
super(_coinConfig);
105+
}
106+
107+
protected get transactionType(): TransactionType {
108+
return TransactionType.CustomTx;
109+
}
110+
111+
initBuilder(tx: Transaction): void {
112+
super.initBuilder(tx);
113+
114+
for (const instruction of this._instructionsData) {
115+
switch (instruction.type) {
116+
case InstructionBuilderTypes.ConfigureConfidentialTransferAccount:
117+
this._configureParams = this._extractConfigureParams(instruction);
118+
break;
119+
case InstructionBuilderTypes.ConfidentialMint:
120+
this._mintParams = this._extractMintParams(instruction);
121+
break;
122+
}
123+
}
124+
}
125+
126+
/**
127+
* Configure a Token-2022 account for confidential transfers.
128+
*
129+
* @param params Configuration parameters.
130+
* @returns This builder.
131+
*/
132+
configureConfidentialTransferAccount(params: ConfigureConfidentialTransferAccountParams): this {
133+
validateAddress(params.tokenAddress, 'tokenAddress');
134+
validateAddress(params.mintAddress, 'mintAddress');
135+
validateAddress(params.authorityAddress, 'authorityAddress');
136+
if (params.instructionsSysvarOrContextStateAddress) {
137+
validateAddress(params.instructionsSysvarOrContextStateAddress, 'instructionsSysvarOrContextStateAddress');
138+
}
139+
this._configureParams = params;
140+
return this;
141+
}
142+
143+
/**
144+
* Set the confidential mint parameters and proof-account metadata.
145+
*
146+
* @param params Mint parameters.
147+
* @returns This builder.
148+
*/
149+
confidentialMint(params: ConfidentialMintParams): this {
150+
validateAddress(params.tokenAddress, 'tokenAddress');
151+
validateAddress(params.mintAddress, 'mintAddress');
152+
validateAddress(params.authorityAddress, 'authorityAddress');
153+
validateAddress(params.payerAddress, 'payerAddress');
154+
validateAddress(params.rangeRecordAddress, 'rangeRecordAddress');
155+
validateAddress(params.equalityContextStateAddress, 'equalityContextStateAddress');
156+
validateAddress(params.validityContextStateAddress, 'validityContextStateAddress');
157+
validateAddress(params.contextStateAuthorityAddress, 'contextStateAuthorityAddress');
158+
if (params.recordAccountOwnerAddress) {
159+
validateAddress(params.recordAccountOwnerAddress, 'recordAccountOwnerAddress');
160+
}
161+
this._mintParams = params;
162+
return this;
163+
}
164+
165+
/** @inheritdoc */
166+
protected async buildImplementation(): Promise<Transaction> {
167+
assert(this._sender, 'Sender must be set before building the transaction');
168+
169+
const instructions: (
170+
| ConfigureConfidentialTransferAccount
171+
| CreateRecordAccount
172+
| WriteRecordData
173+
| VerifyEqualityProof
174+
| VerifyValidityProof
175+
| VerifyRangeProof
176+
| ConfidentialMint
177+
| CloseRecordAccount
178+
| CloseContextState
179+
)[] = [];
180+
181+
if (this._configureParams) {
182+
instructions.push({
183+
type: InstructionBuilderTypes.ConfigureConfidentialTransferAccount,
184+
params: {
185+
tokenAddress: this._configureParams.tokenAddress,
186+
mintAddress: this._configureParams.mintAddress,
187+
authorityAddress: this._configureParams.authorityAddress,
188+
instructionsSysvarOrContextStateAddress:
189+
this._configureParams.instructionsSysvarOrContextStateAddress || INSTRUCTIONS_SYSVAR_ADDRESS,
190+
decryptableZeroBalance: this._configureParams.decryptableZeroBalance,
191+
maximumPendingBalanceCreditCounter: this._configureParams.maximumPendingBalanceCreditCounter,
192+
proofInstructionOffset: this._configureParams.proofInstructionOffset ?? 0,
193+
},
194+
});
195+
}
196+
197+
if (this._mintParams) {
198+
const params = this._mintParams;
199+
const recordOwner = params.recordAccountOwnerAddress || ZK_ELGAMAL_PROOF_PROGRAM_ID;
200+
const rangeRecordBytes = Buffer.from(params.rangeRecordData, 'hex');
201+
const space = rangeRecordBytes.length;
202+
// Solana rent-exempt: (account_size + 128) * rent_per_byte_per_year * 2 years
203+
// rent_per_byte_per_year = 6960 lamports (fixed by Solana runtime)
204+
const lamports = params.recordAccountLamports ?? (space + 128) * 6960 * 2;
205+
206+
instructions.push({
207+
type: InstructionBuilderTypes.CreateRecordAccount,
208+
params: {
209+
payerAddress: params.payerAddress,
210+
recordAccountAddress: params.rangeRecordAddress,
211+
recordAccountOwnerAddress: recordOwner,
212+
space,
213+
lamports,
214+
},
215+
});
216+
217+
instructions.push({
218+
type: InstructionBuilderTypes.WriteRecordData,
219+
params: {
220+
recordAccountAddress: params.rangeRecordAddress,
221+
recordAccountOwnerAddress: recordOwner,
222+
offset: 0,
223+
data: params.rangeRecordData,
224+
},
225+
});
226+
227+
if (params.rangeRecordDataPart2) {
228+
instructions.push({
229+
type: InstructionBuilderTypes.WriteRecordData,
230+
params: {
231+
recordAccountAddress: params.rangeRecordAddress,
232+
recordAccountOwnerAddress: recordOwner,
233+
offset: rangeRecordBytes.length,
234+
data: params.rangeRecordDataPart2,
235+
},
236+
});
237+
}
238+
239+
instructions.push({
240+
type: InstructionBuilderTypes.VerifyEqualityProof,
241+
params: {
242+
proofAccountAddress: params.rangeRecordAddress,
243+
contextStateAccountAddress: params.equalityContextStateAddress,
244+
contextStateAuthorityAddress: params.contextStateAuthorityAddress,
245+
},
246+
});
247+
248+
instructions.push({
249+
type: InstructionBuilderTypes.VerifyValidityProof,
250+
params: {
251+
proofAccountAddress: params.rangeRecordAddress,
252+
contextStateAccountAddress: params.validityContextStateAddress,
253+
contextStateAuthorityAddress: params.contextStateAuthorityAddress,
254+
},
255+
});
256+
257+
// VerifyBatchedRangeProofU128 reads the proof from the record account only;
258+
// it does not use a context state account (unlike equality/validity proofs).
259+
instructions.push({
260+
type: InstructionBuilderTypes.VerifyRangeProof,
261+
params: {
262+
proofAccountAddress: params.rangeRecordAddress,
263+
},
264+
});
265+
266+
instructions.push({
267+
type: InstructionBuilderTypes.ConfidentialMint,
268+
params: {
269+
tokenAddress: params.tokenAddress,
270+
mintAddress: params.mintAddress,
271+
authorityAddress: params.authorityAddress,
272+
equalityRecordAddress: params.equalityContextStateAddress,
273+
ciphertextValidityRecordAddress: params.validityContextStateAddress,
274+
rangeRecordAddress: params.rangeRecordAddress,
275+
newDecryptableSupply: params.newDecryptableSupply,
276+
mintAmountAuditorCiphertextLo: params.mintAmountAuditorCiphertextLo,
277+
mintAmountAuditorCiphertextHi: params.mintAmountAuditorCiphertextHi,
278+
// Offsets are signed i8 relative to confidentialMint; encoded as unsigned u8
279+
// (two's complement: -3 → 253, -2 → 254, -1 → 255)
280+
equalityProofInstructionOffset: (params.equalityProofInstructionOffset ?? -3) & 0xff,
281+
ciphertextValidityProofInstructionOffset: (params.ciphertextValidityProofInstructionOffset ?? -2) & 0xff,
282+
rangeProofInstructionOffset: (params.rangeProofInstructionOffset ?? -1) & 0xff,
283+
},
284+
});
285+
286+
if (params.closeRecordAccount) {
287+
instructions.push({
288+
type: InstructionBuilderTypes.CloseRecordAccount,
289+
params: {
290+
recordAccountAddress: params.rangeRecordAddress,
291+
destinationAddress: params.payerAddress,
292+
authorityAddress: recordOwner,
293+
},
294+
});
295+
}
296+
297+
if (params.closeEqualityContextState) {
298+
instructions.push({
299+
type: InstructionBuilderTypes.CloseContextState,
300+
params: {
301+
contextStateAccountAddress: params.equalityContextStateAddress,
302+
destinationAddress: params.payerAddress,
303+
authorityAddress: params.contextStateAuthorityAddress,
304+
},
305+
});
306+
}
307+
308+
if (params.closeValidityContextState) {
309+
instructions.push({
310+
type: InstructionBuilderTypes.CloseContextState,
311+
params: {
312+
contextStateAccountAddress: params.validityContextStateAddress,
313+
destinationAddress: params.payerAddress,
314+
authorityAddress: params.contextStateAuthorityAddress,
315+
},
316+
});
317+
}
318+
}
319+
320+
if (instructions.length === 0) {
321+
throw new BuildTransactionError(
322+
'A confidential mint or configure instruction must be set before building the transaction'
323+
);
324+
}
325+
326+
this._instructionsData = instructions as InstructionParams[];
327+
return await super.buildImplementation();
328+
}
329+
330+
private _extractConfigureParams(
331+
instruction: ConfigureConfidentialTransferAccount
332+
): ConfigureConfidentialTransferAccountParams {
333+
return {
334+
tokenAddress: instruction.params.tokenAddress,
335+
mintAddress: instruction.params.mintAddress,
336+
authorityAddress: instruction.params.authorityAddress,
337+
instructionsSysvarOrContextStateAddress: instruction.params.instructionsSysvarOrContextStateAddress,
338+
decryptableZeroBalance: instruction.params.decryptableZeroBalance,
339+
maximumPendingBalanceCreditCounter: instruction.params.maximumPendingBalanceCreditCounter,
340+
proofInstructionOffset: instruction.params.proofInstructionOffset,
341+
};
342+
}
343+
344+
private _extractMintParams(instruction: ConfidentialMint): ConfidentialMintParams {
345+
return {
346+
tokenAddress: instruction.params.tokenAddress,
347+
mintAddress: instruction.params.mintAddress,
348+
authorityAddress: instruction.params.authorityAddress,
349+
// Payer cannot be recovered from the instruction alone; default to authority.
350+
payerAddress: instruction.params.authorityAddress,
351+
rangeRecordAddress: instruction.params.rangeRecordAddress || '',
352+
equalityContextStateAddress: instruction.params.equalityRecordAddress || '',
353+
validityContextStateAddress: instruction.params.ciphertextValidityRecordAddress || '',
354+
contextStateAuthorityAddress: instruction.params.authorityAddress,
355+
newDecryptableSupply: instruction.params.newDecryptableSupply,
356+
mintAmountAuditorCiphertextLo: instruction.params.mintAmountAuditorCiphertextLo,
357+
mintAmountAuditorCiphertextHi: instruction.params.mintAmountAuditorCiphertextHi,
358+
rangeRecordData: '',
359+
};
360+
}
361+
}

0 commit comments

Comments
 (0)