Skip to content
Open
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.18.0",
"@bitgo/statics": "^59.12.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
239 changes: 239 additions & 0 deletions modules/sdk-coin-sol/src/lib/confidentialTransferBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { TransactionType } from '@bitgo/sdk-core';
import { Transaction } from './transaction';
import { TransactionBuilder } from './transactionBuilder';
import { InstructionBuilderTypes } from './constants';
import {
ApplyPendingBalance,
ConfidentialDeposit,
ConfidentialTransfer,
ConfidentialWithdraw,
ConfigureConfidentialTransferAccount,
InstructionParams,
VerifyEqualityProof,
VerifyPubkeyValidity,
VerifyRangeProof,
VerifyValidityProof,
} from './iface';
import assert from 'assert';

/**
* Builder for Token-2022 confidential transfer transactions.
*
* Provides type-safe fluent setters for the following CT instructions:
* - ConfigureAccount (standalone, with VerifyPubkeyValidity proof)
* - ApplyPendingBalance (always instruction #1 in v1 spend txs, idempotent)
* - Deposit (public → confidential conversion)
* - Withdraw (confidential → public conversion, with equality + range proofs)
* - Transfer (confidential → confidential, with equality + validity + range proofs)
* - VerifyPubkeyValidity / VerifyEquality / VerifyValidity / VerifyRange proof instructions
*
* Instruction builders are v0/v1-agnostic: they produce instruction data and
* account metas only. The caller (Wallet Platform) assembles them into v1
* transactions in the correct order.
*
* @example
* ```ts
* const builder = factory.getConfidentialTransferBuilder();
* builder.nonce(recentBlockhash).sender(payer);
* builder.configureAccount({ tokenAddress, mintAddress, authorityAddress, ... });
* builder.verifyPubkeyValidity({ proofData });
* const tx = await builder.build();
* ```
*/
export class ConfidentialTransferBuilder extends TransactionBuilder {
private _ctInstructions: InstructionParams[] = [];

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._transaction = new Transaction(_coinConfig);
}

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

/**
* Override the zk-elgamal-proof program id.
*
* Defaults to the canonical on-chain program id (`ZkE1Gama1Proof111...`)
* which is the same across mainnet, devnet, and testnet. Override only
* when targeting a custom deployment.
*
* @param programId - base58 encoded program id
*/
zkProofProgramId(programId: string): this {
assert(programId, 'Missing programId param');
this._zkProofProgramId = programId;
return this;
}

/** @inheritDoc */
initBuilder(tx: Transaction): void {
super.initBuilder(tx);
this._ctInstructions = [];
for (const instruction of this._instructionsData) {
switch (instruction.type) {
case InstructionBuilderTypes.ConfigureConfidentialTransferAccount:
case InstructionBuilderTypes.ApplyPendingBalance:
case InstructionBuilderTypes.ConfidentialDeposit:
case InstructionBuilderTypes.ConfidentialWithdraw:
case InstructionBuilderTypes.ConfidentialTransfer:
case InstructionBuilderTypes.VerifyPubkeyValidity:
case InstructionBuilderTypes.VerifyEqualityProof:
case InstructionBuilderTypes.VerifyValidityProof:
case InstructionBuilderTypes.VerifyRangeProof:
this._ctInstructions.push(instruction);
break;
default:
break;
}
}
}

/**
* Add a ConfigureAccount instruction to the transaction.
* One-time ATA setup — registers ElGamal pubkey + AES zero ciphertext.
* Must be accompanied by a VerifyPubkeyValidity proof instruction.
*/
configureAccount(params: ConfigureConfidentialTransferAccount['params']): this {
assert(params.tokenAddress, 'Missing tokenAddress param');
assert(params.mintAddress, 'Missing mintAddress param');
assert(params.authorityAddress, 'Missing authorityAddress param');
assert(params.decryptableZeroBalance, 'Missing decryptableZeroBalance param');
assert(params.maximumPendingBalanceCreditCounter, 'Missing maximumPendingBalanceCreditCounter param');

this._ctInstructions.push({
type: InstructionBuilderTypes.ConfigureConfidentialTransferAccount,
params,
});
return this;
}

/**
* Add an ApplyPendingBalance instruction to the transaction.
* Credits pending balance into available balance. Idempotent (no-op if 0 pending).
* In v1 transactions, this should always be instruction #1.
*/
applyPendingBalance(params: ApplyPendingBalance['params']): this {
assert(params.tokenAddress, 'Missing tokenAddress param');
assert(params.authorityAddress, 'Missing authorityAddress param');
assert(params.expectedPendingBalanceCreditCounter, 'Missing expectedPendingBalanceCreditCounter param');
assert(params.newDecryptableAvailableBalance, 'Missing newDecryptableAvailableBalance param');

this._ctInstructions.push({
type: InstructionBuilderTypes.ApplyPendingBalance,
params,
});
return this;
}

/**
* Add a Deposit instruction to the transaction.
* Moves public SPL tokens into confidential pending balance. No proof required.
*/
confidentialDeposit(params: ConfidentialDeposit['params']): this {
assert(params.tokenAddress, 'Missing tokenAddress param');
assert(params.mintAddress, 'Missing mintAddress param');
assert(params.authorityAddress, 'Missing authorityAddress param');
assert(params.amount, 'Missing amount param');

this._ctInstructions.push({
type: InstructionBuilderTypes.ConfidentialDeposit,
params,
});
return this;
}

/**
* Add a Withdraw instruction to the transaction.
* Moves confidential available balance to public balance.
* Requires equality + range proof verification instructions.
*/
confidentialWithdraw(params: ConfidentialWithdraw['params']): this {
assert(params.tokenAddress, 'Missing tokenAddress param');
assert(params.mintAddress, 'Missing mintAddress param');
assert(params.authorityAddress, 'Missing authorityAddress param');
assert(params.amount, 'Missing amount param');
assert(params.newDecryptableAvailableBalance, 'Missing newDecryptableAvailableBalance param');

this._ctInstructions.push({
type: InstructionBuilderTypes.ConfidentialWithdraw,
params,
});
return this;
}

/**
* Add a confidential Transfer instruction to the transaction.
* Requires equality + ciphertext validity + range proof verification instructions.
*/
confidentialTransfer(params: ConfidentialTransfer['params']): this {
assert(params.sourceTokenAddress, 'Missing sourceTokenAddress param');
assert(params.mintAddress, 'Missing mintAddress param');
assert(params.destinationTokenAddress, 'Missing destinationTokenAddress param');
assert(params.authorityAddress, 'Missing authorityAddress param');
assert(params.newSourceDecryptableAvailableBalance, 'Missing newSourceDecryptableAvailableBalance param');
assert(params.transferAmountAuditorCiphertextLo, 'Missing transferAmountAuditorCiphertextLo param');
assert(params.transferAmountAuditorCiphertextHi, 'Missing transferAmountAuditorCiphertextHi param');

this._ctInstructions.push({
type: InstructionBuilderTypes.ConfidentialTransfer,
params,
});
return this;
}

/**
* Add a VerifyPubkeyValidity proof instruction (used with ConfigureAccount).
*/
verifyPubkeyValidity(params: VerifyPubkeyValidity['params']): this {
this._ctInstructions.push({
type: InstructionBuilderTypes.VerifyPubkeyValidity,
params,
});
return this;
}

/**
* Add a VerifyCiphertextCommitmentEquality proof instruction (used with Transfer and Withdraw).
*/
verifyEqualityProof(params: VerifyEqualityProof['params']): this {
this._ctInstructions.push({
type: InstructionBuilderTypes.VerifyEqualityProof,
params,
});
return this;
}

/**
* Add a VerifyBatchedGroupedCiphertext3HandlesValidity proof instruction (used with Transfer).
*/
verifyValidityProof(params: VerifyValidityProof['params']): this {
this._ctInstructions.push({
type: InstructionBuilderTypes.VerifyValidityProof,
params,
});
return this;
}

/**
* Add a VerifyBatchedRangeProofU128 proof instruction (used with Transfer).
*/
verifyRangeProof(params: VerifyRangeProof['params']): this {
this._ctInstructions.push({
type: InstructionBuilderTypes.VerifyRangeProof,
params,
});
return this;
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
assert(this._ctInstructions.length > 0, 'At least one confidential transfer instruction must be specified');

this._instructionsData = [...this._ctInstructions];

return await super.buildImplementation();
}
}
38 changes: 38 additions & 0 deletions modules/sdk-coin-sol/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ export const STAKE_ACCOUNT_RENT_EXEMPT_AMOUNT = 2282880;

export const UNAVAILABLE_TEXT = 'UNAVAILABLE';

/**
* Sysvar instructions account address — used by Token-2022 confidential transfer
* instructions to locate inline proof verification instructions in the same transaction.
*/
export const INSTRUCTIONS_SYSVAR_ADDRESS = 'Sysvar1nstructions1111111111111111111111111';

/**
* Canonical on-chain program id of the zk-elgamal-proof program.
*
* This is the same across mainnet, devnet, and testnet — it is a native
* built-in program. Callers can override via ConfidentialTransferBuilder
* .zkProofProgramId() if targeting a custom deployment.
*
* Note: The deprecated zk-token-proof program id is
* `ZkTokenProof1111111111111111111111111111111`.
*/
export const ZK_ELGAMAL_PROOF_PROGRAM_ID = 'ZkE1Gama1Proof11111111111111111111111111111';

/**
* Maximum over-the-wire size of a Solana transaction (in bytes)
*
Expand Down Expand Up @@ -83,6 +101,16 @@ export enum ValidInstructionTypesEnum {
Approve = 'Approve',
CustomInstruction = 'CustomInstruction',
PermissionlessThawIdempotent = 'PermissionlessThawIdempotent',
// Confidential Transfer instruction types (Token-2022 CT extension + zk-elgamal-proof)
ConfigureConfidentialTransferAccount = 'ConfigureConfidentialTransferAccount',
ApplyPendingBalance = 'ApplyPendingBalance',
ConfidentialDeposit = 'ConfidentialDeposit',
ConfidentialWithdraw = 'ConfidentialWithdraw',
ConfidentialTransfer = 'ConfidentialTransfer',
VerifyPubkeyValidity = 'VerifyPubkeyValidity',
VerifyEqualityProof = 'VerifyEqualityProof',
VerifyValidityProof = 'VerifyValidityProof',
VerifyRangeProof = 'VerifyRangeProof',
}

// Internal instructions types
Expand All @@ -109,6 +137,16 @@ export enum InstructionBuilderTypes {
Approve = 'Approve',
WithdrawStake = 'WithdrawStake',
PermissionlessThawIdempotent = 'PermissionlessThawIdempotent',
// Confidential Transfer instruction types (Token-2022 CT extension + zk-elgamal-proof)
ConfigureConfidentialTransferAccount = 'ConfigureConfidentialTransferAccount',
ApplyPendingBalance = 'ApplyPendingBalance',
ConfidentialDeposit = 'ConfidentialDeposit',
ConfidentialWithdraw = 'ConfidentialWithdraw',
ConfidentialTransfer = 'ConfidentialTransfer',
VerifyPubkeyValidity = 'VerifyPubkeyValidity',
VerifyEqualityProof = 'VerifyEqualityProof',
VerifyValidityProof = 'VerifyValidityProof',
VerifyRangeProof = 'VerifyRangeProof',
}

export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [
Expand Down
34 changes: 33 additions & 1 deletion modules/sdk-coin-sol/src/lib/explainTransactionWasm.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ITokenEnablement } from '@bitgo/sdk-core';
import { Transaction, parseTransaction, type ParsedTransaction, type InstructionParams } from '@bitgo/wasm-solana';
import { UNAVAILABLE_TEXT } from './constants';
import { UNAVAILABLE_TEXT, ZK_ELGAMAL_PROOF_PROGRAM_ID } from './constants';
import { StakingAuthorizeParams, TransactionExplanation as SolLibTransactionExplanation } from './iface';
import { findTokenName } from './instructionParamsFactory';
import bs58 from 'bs58';

export interface ExplainTransactionWasmOptions {
txBase64: string;
Expand All @@ -25,6 +26,7 @@ enum TransactionType {
WalletInitialization = 'WalletInitialization',
AssociatedTokenAccountInitialization = 'AssociatedTokenAccountInitialization',
CustomTx = 'CustomTx',
ConfidentialTransfer = 'ConfidentialTransfer',
}

// =============================================================================
Expand Down Expand Up @@ -85,6 +87,33 @@ function detectCombinedPattern(instructions: InstructionParams[]): CombinedPatte

const BOILERPLATE_TYPES = new Set(['NonceAdvance', 'Memo', 'SetComputeUnitLimit', 'SetPriorityFee']);

/**
* Returns true if a WASM-parsed Unknown instruction is a confidential transfer instruction
* (Token-2022 CT extension or zk-elgamal-proof program).
*
* CT extension: byte 0 = 27 (ConfidentialTransferExtension), byte 1 ∈ {2, 5, 6, 7, 8}
* zk-elgamal-proof: any instruction from the zk-elgamal-proof program
*/
function isWasmConfidentialTransferInstruction(instr: InstructionParams): boolean {
if (instr.type !== 'Unknown') return false;
const programId = instr.programId;
// zk-elgamal-proof program instructions are always CT proof verifications
if (programId === ZK_ELGAMAL_PROOF_PROGRAM_ID) {
return true;
}
// Token-2022 CT extension: byte 0 = 27, byte 1 ∈ {2, 5, 6, 7, 8}
const TOKEN_2022_PROGRAM_ID = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
if (programId === TOKEN_2022_PROGRAM_ID) {
const dataBytes = bs58.decode(instr.data);
if (dataBytes.length >= 2) {
const CT_EXT = 27;
const CT_SUB_DISCRIMINATORS = new Set([2, 5, 6, 7, 8]);
return dataBytes[0] === CT_EXT && CT_SUB_DISCRIMINATORS.has(dataBytes[1]);
}
}
return false;
}

function deriveTransactionType(
instructions: InstructionParams[],
combined: CombinedPattern | null,
Expand All @@ -110,6 +139,9 @@ function deriveTransactionType(
if (staking) return TransactionType[staking.type as keyof typeof TransactionType];

// Unknown instructions indicate a custom/unrecognized transaction
if (instructions.some((i) => i.type === 'Unknown' && isWasmConfidentialTransferInstruction(i))) {
return TransactionType.ConfidentialTransfer;
}
if (instructions.some((i) => i.type === 'Unknown')) return TransactionType.CustomTx;

// Send requires an explicit Transfer or TokenTransfer instruction.
Expand Down
Loading
Loading