Skip to content

Commit f0a33c6

Browse files
authored
feat(svm-sdk): add serialized versioned transaction and serialized transaction message to printed svm tx (#8402)
1 parent 084c6b6 commit f0a33c6

4 files changed

Lines changed: 107 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperlane-xyz/sealevel-sdk": minor
3+
---
4+
5+
Added `serializeUnsignedTransaction` to produce base58-encoded unsigned v0 transactions and messages compatible with the Rust Sealevel CLI output. `transactionToPrintableJson` now includes `transactionBase58`, `messageBase58`, and `annotation` fields alongside the existing human-readable format.

typescript/svm-sdk/src/clients/signer.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,16 @@ import { type AltVM } from '@hyperlane-xyz/provider-sdk';
1818
import { assert, rootLogger, strip0x } from '@hyperlane-xyz/utils';
1919

2020
import { createRpc } from '../rpc.js';
21-
import { buildTransactionMessage } from '../tx.js';
22-
import type { SvmReceipt, SvmRpc, SvmTransaction } from '../types.js';
21+
import {
22+
buildTransactionMessage,
23+
serializeUnsignedTransaction,
24+
} from '../tx.js';
25+
import type {
26+
AnnotatedSvmTransaction,
27+
SvmReceipt,
28+
SvmRpc,
29+
SvmTransaction,
30+
} from '../types.js';
2331

2432
import { SvmProvider } from './provider.js';
2533
import { DEFAULT_COMPUTE_UNITS } from '../constants.js';
@@ -117,15 +125,23 @@ export class SvmSigner
117125
}
118126

119127
async transactionToPrintableJson(
120-
transaction: SvmTransaction,
128+
transaction: AnnotatedSvmTransaction,
121129
): Promise<object> {
130+
const { transactionBase58, messageBase58 } = serializeUnsignedTransaction(
131+
transaction.instructions,
132+
this.signer.address,
133+
);
134+
122135
return {
136+
annotation: transaction.annotation,
123137
instructions: transaction.instructions.map((ix) => ({
124138
programAddress: ix.programAddress,
125139
accounts: ix.accounts,
126140
data: ix.data ? Buffer.from(ix.data).toString('hex') : undefined,
127141
})),
128142
computeUnits: transaction.computeUnits,
143+
transaction_base58: transactionBase58,
144+
message_base58: messageBase58,
129145
};
130146
}
131147

typescript/svm-sdk/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ export type {
7575
export { resolveProgram } from './deploy/resolve-program.js';
7676

7777
// Transaction utilities
78-
export { getComputeBudgetInstructions, buildTransactionMessage } from './tx.js';
78+
export {
79+
getComputeBudgetInstructions,
80+
buildTransactionMessage,
81+
serializeUnsignedTransaction,
82+
} from './tx.js';
7983

8084
// PDA derivation
8185
export {

typescript/svm-sdk/src/tx.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)