Many thanks to @Jordan! After digging a bit more I think I got to a decent solution.
Here is the breakdown. For each tx we have:
- a compact array of signatures,
64 bytes
each
- a header array with: # signatures, # read-only accounts that need signatures, # read-only accounts that don't need signatures,
3 bytes
total
- a compact array of accounts,
32 bytes
each
- a blockhash,
32 bytes
- a compact array of instructions.
Each instruction has this format:
- index of program id,
1 byte
- compact array of account indexes,
1 byte
for each index
- compact array of opaque bytes,
1 byte
each
Moreover, whenever there is a compact array, we need to add 1-3 bytes
depending on how many elements are there in the array, more info here.
This is the code, I tested it against several txs and seems to work well (legacy transactions only):
import { PublicKey, Transaction } from '@solana/web3.js';
/**
* @param tx a solana transaction
* @param feePayer the publicKey of the signer
* @returns size in bytes of the transaction
*/
const getTxSize = (tx: Transaction, feePayer: PublicKey): number => {
const feePayerPk = [feePayer.toBase58()];
const signers = new Set<string>(feePayerPk);
const accounts = new Set<string>(feePayerPk);
const ixsSize = tx.instructions.reduce((acc, ix) => {
ix.keys.forEach(({ pubkey, isSigner }) => {
const pk = pubkey.toBase58();
if (isSigner) signers.add(pk);
accounts.add(pk);
});
accounts.add(ix.programId.toBase58());
const nIndexes = ix.keys.length;
const opaqueData = ix.data.length;
return (
acc +
1 + // PID index
compactArraySize(nIndexes, 1) +
compactArraySize(opaqueData, 1)
);
}, 0);
return (
compactArraySize(signers.size, 64) + // signatures
3 + // header
compactArraySize(accounts.size, 32) + // accounts
32 + // blockhash
compactHeader(tx.instructions.length) + // instructions
ixsSize
);
};
// COMPACT ARRAY
const LOW_VALUE = 127; // 0x7f
const HIGH_VALUE = 16383; // 0x3fff
/**
* Compact u16 array header size
* @param n elements in the compact array
* @returns size in bytes of array header
*/
const compactHeader = (n: number) => (n <= LOW_VALUE ? 1 : n <= HIGH_VALUE ? 2 : 3);
/**
* Compact u16 array size
* @param n elements in the compact array
* @param size bytes per each element
* @returns size in bytes of array
*/
const compactArraySize = (n: number, size: number) => compactHeader(n) + n * size;