11import { BaseCoin as CoinConfig } from '@bitgo/statics' ;
2- import { TransactionType } from '@bitgo/sdk-core' ;
2+ import { BuildTransactionError , SolTransactionVersion , SolV1TransactionConfig , TransactionType } from '@bitgo/sdk-core' ;
3+ import { PublicKey , Transaction as SolTransaction , TransactionInstruction } from '@solana/web3.js' ;
4+ import nacl from 'tweetnacl' ;
35import { Transaction } from './transaction' ;
46import { TransactionBuilder } from './transactionBuilder' ;
57import { InstructionBuilderTypes } from './constants' ;
@@ -10,11 +12,15 @@ import {
1012 ConfidentialWithdraw ,
1113 ConfigureConfidentialTransferAccount ,
1214 InstructionParams ,
15+ Memo ,
1316 VerifyEqualityProof ,
1417 VerifyPubkeyValidity ,
1518 VerifyRangeProof ,
1619 VerifyValidityProof ,
1720} from './iface' ;
21+ import { compileTransactionMessage } from './serialization/compileTransactionMessage' ;
22+ import { serializeWireTransaction } from './serialization/wire-transaction' ;
23+ import { solInstructionFactory } from './solInstructionFactory' ;
1824import assert from 'assert' ;
1925
2026/**
@@ -43,6 +49,8 @@ import assert from 'assert';
4349 */
4450export class ConfidentialTransferBuilder extends TransactionBuilder {
4551 private _ctInstructions : InstructionParams [ ] = [ ] ;
52+ private _version ?: SolTransactionVersion ;
53+ private _v1TransactionConfig ?: SolV1TransactionConfig ;
4654
4755 constructor ( _coinConfig : Readonly < CoinConfig > ) {
4856 super ( _coinConfig ) ;
@@ -53,6 +61,35 @@ export class ConfidentialTransferBuilder extends TransactionBuilder {
5361 return TransactionType . ConfidentialTransfer ;
5462 }
5563
64+ /**
65+ * Set the Solana transaction version.
66+ *
67+ * Defaults to legacy until set. When set to `1`, the builder assembles a v1
68+ * (SIMD-0296/0385) transaction: version byte `0x81`, an inline `transactionConfig`
69+ * instead of ComputeBudget instructions, no address lookup tables, and a
70+ * message-first wire format with signatures appended.
71+ *
72+ * @param version - the transaction version (0 = v0, 1 = v1)
73+ * @returns {this } This builder
74+ */
75+ version ( version : SolTransactionVersion ) : this {
76+ this . _version = version ;
77+ return this ;
78+ }
79+
80+ /**
81+ * Set the v1 transaction config (compute unit limit, heap size, loaded accounts
82+ * data size limit, and priority fee as total lamports). Required when
83+ * `version(1)` is set.
84+ *
85+ * @param config - the v1 transaction config
86+ * @returns {this } This builder
87+ */
88+ transactionConfig ( config : SolV1TransactionConfig ) : this {
89+ this . _v1TransactionConfig = config ;
90+ return this ;
91+ }
92+
5693 /**
5794 * Override the zk-elgamal-proof program id.
5895 *
@@ -232,8 +269,87 @@ export class ConfidentialTransferBuilder extends TransactionBuilder {
232269 protected async buildImplementation ( ) : Promise < Transaction > {
233270 assert ( this . _ctInstructions . length > 0 , 'At least one confidential transfer instruction must be specified' ) ;
234271
272+ if ( this . _version === 1 ) {
273+ return this . buildV1 ( ) ;
274+ }
275+
235276 this . _instructionsData = [ ...this . _ctInstructions ] ;
236277
237278 return await super . buildImplementation ( ) ;
238279 }
280+
281+ /**
282+ * Build a v1 (SIMD-0296/0385) confidential transfer transaction.
283+ *
284+ * Assembles the CT instructions via the shared instruction factory, compiles
285+ * and serializes a v1 message with the inline transaction config, signs the
286+ * message bytes with the builder's signers, and stores the resulting wire
287+ * bytes for broadcast. Also populates a metadata-only SolTransaction so the
288+ * transaction JSON and input/output extraction remain usable.
289+ *
290+ * @returns {Transaction } The built transaction holding the v1 wire bytes
291+ */
292+ private buildV1 ( ) : Transaction {
293+ assert ( this . _sender , new BuildTransactionError ( 'sender is required before building' ) ) ;
294+ assert ( this . _recentBlockhash , new BuildTransactionError ( 'recent blockhash is required before building' ) ) ;
295+ assert ( this . _v1TransactionConfig , 'transactionConfig is required to build a v1 confidential transfer transaction' ) ;
296+
297+ // ApplyPendingBalance must remain instruction #1 in v1 spend txs (it is idempotent),
298+ // regardless of the order the caller added instructions.
299+ this . _ctInstructions = [
300+ ...this . _ctInstructions . filter ( ( i ) => i . type === InstructionBuilderTypes . ApplyPendingBalance ) ,
301+ ...this . _ctInstructions . filter ( ( i ) => i . type !== InstructionBuilderTypes . ApplyPendingBalance ) ,
302+ ] ;
303+
304+ const instructions : TransactionInstruction [ ] = [ ] ;
305+ for ( const instruction of this . _ctInstructions ) {
306+ instructions . push ( ...solInstructionFactory ( instruction , this . _zkProofProgramId ) ) ;
307+ }
308+
309+ if ( this . _memo ) {
310+ const memoData : Memo = {
311+ type : InstructionBuilderTypes . Memo ,
312+ params : { memo : this . _memo } ,
313+ } ;
314+ this . _ctInstructions . push ( memoData ) ;
315+ instructions . push ( ...solInstructionFactory ( memoData ) ) ;
316+ }
317+
318+ const feePayer = this . _feePayer ? new PublicKey ( this . _feePayer ) : new PublicKey ( this . _sender ) ;
319+ const messageBytes = compileTransactionMessage ( {
320+ version : 1 ,
321+ instructions,
322+ feePayer,
323+ recentBlockhash : this . _recentBlockhash ,
324+ transactionConfig : this . _v1TransactionConfig ,
325+ } ) ;
326+
327+ const signatures : Uint8Array [ ] = [ ] ;
328+ for ( const signer of this . _signers ) {
329+ const secretKey = signer . getKeys ( true ) . prv ;
330+ assert ( secretKey instanceof Uint8Array , 'Missing private key' ) ;
331+ signatures . push ( nacl . sign . detached ( messageBytes , secretKey ) ) ;
332+ }
333+ for ( const signature of this . getAdditionalSignatures ( ) ) {
334+ signatures . push ( new Uint8Array ( signature . signature ) ) ;
335+ }
336+
337+ const v1Wire = serializeWireTransaction ( messageBytes , signatures ) ;
338+
339+ this . _transaction . v1TransactionBytes = v1Wire ;
340+ this . _transaction . v1MessageBytes = messageBytes ;
341+
342+ // Populate a metadata-only SolTransaction so toJson / loadInputsAndOutputs work.
343+ const metaTx = new SolTransaction ( ) ;
344+ metaTx . feePayer = feePayer ;
345+ metaTx . recentBlockhash = this . _recentBlockhash ;
346+ metaTx . add ( ...instructions ) ;
347+ this . _transaction . solTransaction = metaTx ;
348+
349+ this . _transaction . setTransactionType ( this . transactionType ) ;
350+ this . _transaction . setInstructionsData ( this . _ctInstructions ) ;
351+ this . _transaction . loadInputsAndOutputs ( ) ;
352+
353+ return this . _transaction ;
354+ }
239355}
0 commit comments