Sign transactions
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]);
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.
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.
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.
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 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();