web3.js v2 'Attempted to sign a transaction with an address that is not a signer' — add AccountRole.READONLY_SIGNER meta
Symptom: with @solana/web3.js v2 / @solana/kit you build a transfer where the user is fee payer and source, compile it, then call partiallySignTransaction([feeReceiver.keyPair], compiledMessage) so your own key co-signs. It throws Attempted to sign a transaction with an address that is not a signer for it.
Cause: the signer set of a compiled transaction is derived entirely from the message — the fee payer plus every account meta with a signer role. In a SOL transfer the fee payer and the source are both the user; feeReceiver appears only as the transfer *destination*, which is writable but not a signer. There is nowhere for its signature to go, so signing is rejected. Passing a keypair to the signing function does not, by itself, make that address a signer.
Fix: give the address a signer role somewhere in the message before compiling. The cheapest trick is a no-op instruction with an extra signer meta:
const memo = getAddMemoInstruction({ memo: '' });
const memoWithSigner = {
...memo,
accounts: [...(memo.accounts ?? []), { address: feeReceiver.address, role: AccountRole.READONLY_SIGNER }],
};Append that alongside the transfer; compiling now requires both signatures and partiallySignTransaction succeeds. Alternatively clone the transfer instruction and replace the feeReceiver meta with a signer role, keeping it at the same index in accounts.
Why you would want this: a co-signature is what makes a partially signed transaction tamper-evident — the user cannot alter instructions without invalidating your signature. Also remember the account order in accounts is positional for the program, so only add metas the program ignores (memo ignores extras).
Source: adapted from a Stack Exchange answer by Callum M (question by matt), https://solana.stackexchange.com/questions/20190/how-to-using-new-solana-web3-js-to-complete-partiallysigntransaction — CC BY-SA 4.0