Skip to content
Draft
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
1 change: 1 addition & 0 deletions modules/sdk-coin-sol/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@bitgo/sdk-lib-mpc": "^10.15.0",
"@bitgo/statics": "^59.0.0",
"@bitgo/wasm-solana": "^2.6.0",
"@solana/buffer-layout": "^4.0.1",
"@solana/spl-stake-pool": "1.1.8",
"@solana/spl-token": "0.4.9",
"@solana/web3.js": "1.92.1",
Expand Down
355 changes: 355 additions & 0 deletions modules/sdk-coin-sol/src/lib/confidentialMintBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
import { Transaction } from './transaction';
import { TransactionBuilder } from './transactionBuilder';
import { INSTRUCTIONS_SYSVAR_ADDRESS, InstructionBuilderTypes } from './constants';
import {
ConfidentialMint,
ConfigureConfidentialTransferAccount,
CreateRecordAccount,
WriteRecordData,
VerifyEqualityProof,
VerifyValidityProof,
VerifyRangeProof,
CloseRecordAccount,
CloseContextState,
InstructionParams,
} from './iface';
import { validateAddress } from './utils';
import assert from 'assert';

/**
* Parameters for the confidential mint proof-account group.
*/
export interface ConfidentialMintParams {
/** Token account receiving the confidential mint (must be Token-2022). */
tokenAddress: string;
/** Mint address (must be Token-2022 with ConfidentialTransferMint configured). */
mintAddress: string;
/** Mint authority that must sign the confidentialMint instruction. */
authorityAddress: string;
/** Payer / fee payer for creating proof accounts. */
payerAddress: string;
/** Address of the reusable record account for the range proof. */
rangeRecordAddress: string;
/** Address of the one-shot context state account for the equality proof. */
equalityContextStateAddress: string;
/** Address of the one-shot context state account for the ciphertext validity proof. */
validityContextStateAddress: string;
/** Authority that owns the context state accounts (usually the payer). */
contextStateAuthorityAddress: string;
/** Authority that owns the reusable record account. */
recordAccountOwnerAddress?: string;
/** 36-byte hex decryptable ciphertext of the new supply. */
newDecryptableSupply: string;
/** 64-byte hex low half of the auditor ciphertext. */
mintAmountAuditorCiphertextLo: string;
/** 64-byte hex high half of the auditor ciphertext. */
mintAmountAuditorCiphertextHi: string;
/** Hex data for the record account write (typically the 1000B BatchedRangeProofU128). */
rangeRecordData: string;
/** Optional second chunk of record data for 1-2 writes. */
rangeRecordDataPart2?: string;
/** Instruction offset to the equality proof verify instruction (relative to confidentialMint). */
equalityProofInstructionOffset?: number;
/** Instruction offset to the ciphertext validity proof verify instruction. */
ciphertextValidityProofInstructionOffset?: number;
/** Instruction offset to the range proof verify instruction. */
rangeProofInstructionOffset?: number;
/** Optional close the range record account after mint. */
closeRecordAccount?: boolean;
/** Optional close the equality context state account after mint. */
closeEqualityContextState?: boolean;
/** Optional close the validity context state account after mint. */
closeValidityContextState?: boolean;
}

/**
* Parameters for configuring a confidential transfer token account.
*/
export interface ConfigureConfidentialTransferAccountParams {
/** Token account to configure. */
tokenAddress: string;
/** Mint address. */
mintAddress: string;
/** Account owner / authority that must sign. */
authorityAddress: string;
/** Optional instructions sysvar or context state address for proof verification. */
instructionsSysvarOrContextStateAddress?: string;
/** 36-byte hex decryptable zero balance ciphertext. */
decryptableZeroBalance: string;
/** Maximum pending balance credit counter as a decimal string. */
maximumPendingBalanceCreditCounter: string;
/** Offset to the proof instruction (usually 0 when proof is in same tx). */
proofInstructionOffset?: number;
}

/**
* Builder for Solana Token-2022 confidential mint transactions using the proof-account approach.
*
* This builder groups the proof-account tx sequence as one logical transaction:
* create-record → write(1-2) → verify equality → verify validity → verify range → confidentialMint
* → optional close record/context-state accounts.
*
* The actual zero-knowledge proofs are generated externally (e.g. KMS worker); the SDK only
* assembles instruction data and account metadata.
*/
export class ConfidentialMintBuilder extends TransactionBuilder {
private _mintParams?: ConfidentialMintParams;
private _configureParams?: ConfigureConfidentialTransferAccountParams;

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}

protected get transactionType(): TransactionType {
return TransactionType.CustomTx;
}

initBuilder(tx: Transaction): void {
super.initBuilder(tx);

for (const instruction of this._instructionsData) {
switch (instruction.type) {
case InstructionBuilderTypes.ConfigureConfidentialTransferAccount:
this._configureParams = this._extractConfigureParams(instruction);
break;
case InstructionBuilderTypes.ConfidentialMint:
this._mintParams = this._extractMintParams(instruction);
break;
}
}
}

/**
* Configure a Token-2022 account for confidential transfers.
*
* @param params Configuration parameters.
* @returns This builder.
*/
configureConfidentialTransferAccount(params: ConfigureConfidentialTransferAccountParams): this {
validateAddress(params.tokenAddress, 'tokenAddress');
validateAddress(params.mintAddress, 'mintAddress');
validateAddress(params.authorityAddress, 'authorityAddress');
if (params.instructionsSysvarOrContextStateAddress) {
validateAddress(params.instructionsSysvarOrContextStateAddress, 'instructionsSysvarOrContextStateAddress');
}
this._configureParams = params;
return this;
}

/**
* Set the confidential mint parameters and proof-account metadata.
*
* @param params Mint parameters.
* @returns This builder.
*/
confidentialMint(params: ConfidentialMintParams): this {
validateAddress(params.tokenAddress, 'tokenAddress');
validateAddress(params.mintAddress, 'mintAddress');
validateAddress(params.authorityAddress, 'authorityAddress');
validateAddress(params.payerAddress, 'payerAddress');
validateAddress(params.rangeRecordAddress, 'rangeRecordAddress');
validateAddress(params.equalityContextStateAddress, 'equalityContextStateAddress');
validateAddress(params.validityContextStateAddress, 'validityContextStateAddress');
validateAddress(params.contextStateAuthorityAddress, 'contextStateAuthorityAddress');
if (params.recordAccountOwnerAddress) {
validateAddress(params.recordAccountOwnerAddress, 'recordAccountOwnerAddress');
}
this._mintParams = params;
return this;
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
assert(this._sender, 'Sender must be set before building the transaction');

const instructions: (
| ConfigureConfidentialTransferAccount
| CreateRecordAccount
| WriteRecordData
| VerifyEqualityProof
| VerifyValidityProof
| VerifyRangeProof
| ConfidentialMint
| CloseRecordAccount
| CloseContextState
)[] = [];

if (this._configureParams) {
instructions.push({
type: InstructionBuilderTypes.ConfigureConfidentialTransferAccount,
params: {
tokenAddress: this._configureParams.tokenAddress,
mintAddress: this._configureParams.mintAddress,
authorityAddress: this._configureParams.authorityAddress,
instructionsSysvarOrContextStateAddress:
this._configureParams.instructionsSysvarOrContextStateAddress || INSTRUCTIONS_SYSVAR_ADDRESS,
decryptableZeroBalance: this._configureParams.decryptableZeroBalance,
maximumPendingBalanceCreditCounter: this._configureParams.maximumPendingBalanceCreditCounter,
proofInstructionOffset: this._configureParams.proofInstructionOffset ?? 0,
},
});
}

if (this._mintParams) {
const params = this._mintParams;
const recordOwner = params.recordAccountOwnerAddress || params.contextStateAuthorityAddress;
const rangeRecordBytes = Buffer.from(params.rangeRecordData, 'hex');
const space = rangeRecordBytes.length;

instructions.push({
type: InstructionBuilderTypes.CreateRecordAccount,
params: {
payerAddress: params.payerAddress,
recordAccountAddress: params.rangeRecordAddress,
recordAccountOwnerAddress: recordOwner,
space,
},
});

instructions.push({
type: InstructionBuilderTypes.WriteRecordData,
params: {
recordAccountAddress: params.rangeRecordAddress,
recordAccountOwnerAddress: recordOwner,
offset: 0,
data: params.rangeRecordData,
},
});

if (params.rangeRecordDataPart2) {
instructions.push({
type: InstructionBuilderTypes.WriteRecordData,
params: {
recordAccountAddress: params.rangeRecordAddress,
recordAccountOwnerAddress: recordOwner,
offset: rangeRecordBytes.length,
data: params.rangeRecordDataPart2,
},
});
}

instructions.push({
type: InstructionBuilderTypes.VerifyEqualityProof,
params: {
proofAccountAddress: params.rangeRecordAddress,
contextStateAccountAddress: params.equalityContextStateAddress,
contextStateAuthorityAddress: params.contextStateAuthorityAddress,
},
});

instructions.push({
type: InstructionBuilderTypes.VerifyValidityProof,
params: {
proofAccountAddress: params.rangeRecordAddress,
contextStateAccountAddress: params.validityContextStateAddress,
contextStateAuthorityAddress: params.contextStateAuthorityAddress,
},
});

// The range proof context state is stored in the same reusable record account used for the
// proof data; the equality/validity proofs each have their own one-shot context state account.
instructions.push({
type: InstructionBuilderTypes.VerifyRangeProof,
params: {
proofAccountAddress: params.rangeRecordAddress,
contextStateAccountAddress: params.rangeRecordAddress,
contextStateAuthorityAddress: params.contextStateAuthorityAddress,
},
});

instructions.push({
type: InstructionBuilderTypes.ConfidentialMint,
params: {
tokenAddress: params.tokenAddress,
mintAddress: params.mintAddress,
authorityAddress: params.authorityAddress,
equalityRecordAddress: params.equalityContextStateAddress,
ciphertextValidityRecordAddress: params.validityContextStateAddress,
rangeRecordAddress: params.rangeRecordAddress,
newDecryptableSupply: params.newDecryptableSupply,
mintAmountAuditorCiphertextLo: params.mintAmountAuditorCiphertextLo,
mintAmountAuditorCiphertextHi: params.mintAmountAuditorCiphertextHi,
equalityProofInstructionOffset: params.equalityProofInstructionOffset ?? 5,
ciphertextValidityProofInstructionOffset: params.ciphertextValidityProofInstructionOffset ?? 4,
rangeProofInstructionOffset: params.rangeProofInstructionOffset ?? 3,
},
});

if (params.closeRecordAccount) {
instructions.push({
type: InstructionBuilderTypes.CloseRecordAccount,
params: {
recordAccountAddress: params.rangeRecordAddress,
destinationAddress: params.payerAddress,
authorityAddress: recordOwner,
},
});
}

if (params.closeEqualityContextState) {
instructions.push({
type: InstructionBuilderTypes.CloseContextState,
params: {
contextStateAccountAddress: params.equalityContextStateAddress,
destinationAddress: params.payerAddress,
authorityAddress: params.contextStateAuthorityAddress,
},
});
}

if (params.closeValidityContextState) {
instructions.push({
type: InstructionBuilderTypes.CloseContextState,
params: {
contextStateAccountAddress: params.validityContextStateAddress,
destinationAddress: params.payerAddress,
authorityAddress: params.contextStateAuthorityAddress,
},
});
}
}

if (instructions.length === 0) {
throw new BuildTransactionError(
'A confidential mint or configure instruction must be set before building the transaction'
);
}

this._instructionsData = instructions as InstructionParams[];
return await super.buildImplementation();
}

private _extractConfigureParams(
instruction: ConfigureConfidentialTransferAccount
): ConfigureConfidentialTransferAccountParams {
return {
tokenAddress: instruction.params.tokenAddress,
mintAddress: instruction.params.mintAddress,
authorityAddress: instruction.params.authorityAddress,
instructionsSysvarOrContextStateAddress: instruction.params.instructionsSysvarOrContextStateAddress,
decryptableZeroBalance: instruction.params.decryptableZeroBalance,
maximumPendingBalanceCreditCounter: instruction.params.maximumPendingBalanceCreditCounter,
proofInstructionOffset: instruction.params.proofInstructionOffset,
};
}

private _extractMintParams(instruction: ConfidentialMint): ConfidentialMintParams {
return {
tokenAddress: instruction.params.tokenAddress,
mintAddress: instruction.params.mintAddress,
authorityAddress: instruction.params.authorityAddress,
// Payer cannot be recovered from the instruction alone; default to authority.
payerAddress: instruction.params.authorityAddress,
rangeRecordAddress: instruction.params.rangeRecordAddress || '',
equalityContextStateAddress: instruction.params.equalityRecordAddress || '',
validityContextStateAddress: instruction.params.ciphertextValidityRecordAddress || '',
contextStateAuthorityAddress: instruction.params.authorityAddress,
newDecryptableSupply: instruction.params.newDecryptableSupply,
mintAmountAuditorCiphertextLo: instruction.params.mintAmountAuditorCiphertextLo,
mintAmountAuditorCiphertextHi: instruction.params.mintAmountAuditorCiphertextHi,
rangeRecordData: '',
};
}
}
Loading
Loading