@@ -2,9 +2,16 @@ import {
22 type Address ,
33 type Blockhash ,
44 type Instruction ,
5+ type ReadonlyUint8Array ,
56 type TransactionSigner ,
67 appendTransactionMessageInstructions ,
8+ blockhash ,
9+ compileTransactionMessage ,
710 createTransactionMessage ,
11+ getBase58Decoder ,
12+ getCompiledTransactionMessageEncoder ,
13+ getShortU16Encoder ,
14+ setTransactionMessageFeePayer ,
815 setTransactionMessageFeePayerSigner ,
916 setTransactionMessageLifetimeUsingBlockhash ,
1017} from '@solana/kit' ;
@@ -92,3 +99,74 @@ export function transactionToInstructions(
9299 const computeBudgetIxs = getComputeBudgetInstructions ( computeUnits ) ;
93100 return [ ...computeBudgetIxs , ...tx . instructions ] ;
94101}
102+
103+ // ---------------------------------------------------------------------------
104+ // Unsigned transaction serialization (Squads-compatible v0 format)
105+ // ---------------------------------------------------------------------------
106+
107+ const base58Decoder = getBase58Decoder ( ) ;
108+ const messageEncoder = getCompiledTransactionMessageEncoder ( ) ;
109+ const shortU16Encoder = getShortU16Encoder ( ) ;
110+
111+ /** Default blockhash (32 zero bytes) that needs to be replaced at submission time */
112+ const DEFAULT_BLOCKHASH = blockhash ( '11111111111111111111111111111111' ) ;
113+
114+ /**
115+ * Builds the wire bytes of an unsigned versioned (v0) transaction.
116+ * Prepends compact-u16 signature count + N zero-filled 64-byte signature
117+ * slots to the compiled message bytes.
118+ */
119+ function buildUnsignedTransactionBytes (
120+ numSigners : number ,
121+ messageBytes : ReadonlyUint8Array ,
122+ ) : Uint8Array {
123+ const sigCountBytes = shortU16Encoder . encode ( numSigners ) ;
124+ const sigsLen = numSigners * 64 ;
125+ const result = new Uint8Array (
126+ sigCountBytes . length + sigsLen + messageBytes . length ,
127+ ) ;
128+ result . set ( sigCountBytes , 0 ) ;
129+ // signature slots are already zero-filled by Uint8Array constructor
130+ result . set ( messageBytes , sigCountBytes . length + sigsLen ) ;
131+ return result ;
132+ }
133+
134+ /**
135+ * Serializes an SvmTransaction into base58-encoded formats compatible
136+ * with the Squads multisig UI.
137+ *
138+ * Produces two representations:
139+ * - `transaction_base58`: full unsigned v0 transaction (signatures + message)
140+ * - `message_base58`: compiled message only (no signature wrapper)
141+ *
142+ * Both use a default (all-zeros) blockhash since these are unsigned
143+ * transactions intended for offline / multisig signing workflows.
144+ */
145+ export function serializeUnsignedTransaction (
146+ instructions : SvmInstruction [ ] ,
147+ feePayer : Address ,
148+ ) : { transactionBase58 : string ; messageBase58 : string } {
149+ const txMessage = createTransactionMessage ( { version : 0 } ) ;
150+ const withFeePayer = setTransactionMessageFeePayer ( feePayer , txMessage ) ;
151+ const withLifetime = setTransactionMessageLifetimeUsingBlockhash (
152+ { blockhash : DEFAULT_BLOCKHASH , lastValidBlockHeight : 0n } ,
153+ withFeePayer ,
154+ ) ;
155+ const withInstructions = appendTransactionMessageInstructions (
156+ instructions ,
157+ withLifetime ,
158+ ) ;
159+
160+ const compiled = compileTransactionMessage ( withInstructions ) ;
161+ const messageBytes = messageEncoder . encode ( compiled ) ;
162+
163+ const transactionBytes = buildUnsignedTransactionBytes (
164+ compiled . header . numSignerAccounts ,
165+ messageBytes ,
166+ ) ;
167+
168+ return {
169+ transactionBase58 : base58Decoder . decode ( transactionBytes ) ,
170+ messageBase58 : base58Decoder . decode ( messageBytes ) ,
171+ } ;
172+ }
0 commit comments