TransactionMessage Requirements

In order to be signable, a TransactionMessage must:

  • have a version and a list of zero or more instructions (ie. conform to BaseTransactionMessage)
  • have a fee payer set (ie. conform to TransactionMessageWithFeePayer)
  • have a lifetime specified (ie. conform to TransactionMessageWithBlockhashLifetime | TransactionMessageWithDurableNonceLifetime)

On the client, signers abstract the interface to sign transactions.

A signer is an interface that hides the implementation details of a specific signer (like a wallet in the browser).

await mySigner.signTransactions([myTransaction]);

Usually we construct the transaction by building up a bunch of instructions:

import { pipe } from "@solana/functional";
import { createTransactionMessage } from "@solana/transaction-messages";
import { compileTransaction } from "@solana/transactions";

const myTransactionMessage = pipe(
    createTransactionMessage({ version: 0 }),
    // Add instructions, fee payer, lifetime, etc.
);
const myTransaction = compileTransaction(myTransactionMessage);
const [transactionSignatures] = await mySigner.signTransactions([myTransaction]);

Categories of Signers

There are 3 categories of signers:

  • Partial signers: Given a message or transaction, provide one or more signatures for it. These signers are not able to modify the given data which allows us to run many of them in parallel.
  • Modifying signers: Can choose to modify a message or transaction before signing it with zero or more private keys. Because modifying a message or transaction invalidates any pre-existing signatures over it, modifying signers must do their work before any other signer.
  • Sending signers: Given a transaction, signs it and sends it immediately to the blockchain. When applicable, the signer may also decide to modify the provided transaction before signing it. This interface accommodates wallets that simply cannot sign a transaction without sending it at the same time. This category of signers does not apply to regular messages.

Signer Types Table

Here is a table of the various signer types for both messages and transactions.

Partial signers Modifying signers Sending signers
TransactionSigner TransactionPartialSigner TransactionModifyingSigner TransactionSendingSigner
MessageSigner MessagePartialSigner MessageModifyingSigner N/A

Signers can be stored inside of the account meta of an instruction. This helps avoid passing them around as plain old addresses.

Creating Signable Messages

To sign something we need at least a signable message:

import { createSignableMessage } from "@solana/kit"

const myMessage = createSignableMessage(new Uint8Array([1, 2, 3]));
const myMessageFromText = createSignableMessage('Hello world!');
const myMessageWithSignatures = createSignableMessage('Hello world!', {
    '1234..5678': new Uint8Array([1, 2, 3]),
});

A message is just a recent blockhash, a list of accounts and a corresponding list of instructions.

Usually though you would be signing an actual Transaction which is a message and a list of signers.

Union Types for Signers

Most of the time you will be working with the union types:

type MessageSigner<TAddress extends string = string> =
    | MessagePartialSigner<TAddress>
    | MessageModifyingSigner<TAddress>;

type TransactionSigner<TAddress extends string = string> =
    | TransactionPartialSigner<TAddress>
    | TransactionModifyingSigner<TAddress>
    | TransactionSendingSigner<TAddress>;

Most of the time in our tests we are using a KeyPairSigner

import { generateKeyPair } from "@solana/keys";
import { createSignerFromKeyPair, KeyPairSigner } from "@solana/signers";

const myKeyPair: CryptoKeyPair = await generateKeyPair();
const myKeyPairSigner: KeyPairSigner = await createSignerFromKeyPair(myKeyPair);

And even more likely we would use the helper:

import { generateKeyPairSigner } from "@solana/signers";

const myKeyPairSigner = await generateKeyPairSigner();