Features
Public keys and signers
On this page, we'll see how to manage public keys and signers in Umi which is partially made possible by the EdDSA interface.
The EdDSA interface is used to create keypairs, find PDAs and sign/verify messages using the EdDSA algorithm. We can either use this interface directly and/or use helper methods that delegate to this interface to provide a better developer experience.
Let's tackle this on a per-use case basis.
Public keys
In Umi, a public key is a simple base58 string
representing a 32-byte array. We use an opaque type to tell TypeScript that the given public key has been verified and is valid. We also use a type parameter to offer more granular type safety.
// In short:
type PublicKey = string;
// In reality:
type PublicKey<TAddress extends string = string> = TAddress & { __publicKey: unique symbol };
We can create a new valid public key from a variety of inputs using the publicKey
helper method. If the provided input cannot be converted to a valid public key, an error will be thrown.
// From a base58 string.
publicKey('LorisCg1FTs89a32VSrFskYDgiRbNQzct1WxyZb7nuA');
// From a 32-byte buffer.
publicKey(new Uint8Array(32));
// From a PublicKey or Signer type.
publicKey(someWallet as PublicKey | Signer);
It is possible to convert a public key to a Uint8Array
using the publicKeyBytes
helper method.
publicKeyBytes(myPublicKey);
// -> Uint8Array(32)
Additional helper methods are also available to help manage public keys.
// Check if the provided value is a valid public key.
isPublicKey(myPublicKey);
// Assert the provided value is a valid public key and fail otherwise.
assertPublicKey(myPublicKey);
// Deduplicate an array of public keys.
uniquePublicKeys(myPublicKeys);
// Create the default public key which is a 32-bytes array of zeros.
defaultPublicKey();
PDAs
A PDA — or Program-Derived Address — is a public key that is derived from a program ID and an array of predefined seeds. A bump
number ranging from 0 to 255 is required to ensure the PDA does not live on the EdDSA elliptic curve and therefore does not conflict with cryptographically generated public keys.
In Umi, PDAs are represented as a tuple composed of the derived public key and the bump number. Similarly to public keys, it uses opaque types and type parameters.
// In short:
type Pda = [PublicKey, number];
// In reality:
export type Pda<
TAddress extends string = string,
TBump extends number = number
> = [PublicKey<TAddress>, TBump] & { readonly __pda: unique symbol };
To derive a new PDA, we can use the findPda
method of the EdDSA interface.
const pda = umi.eddsa.findPda(programId, seeds);
Each seed must be serialized as a Uint8Array
. You can learn more about serializers on the Serializers page but here is a quick example showing how to find the metadata PDA of a given mint address.
import { publicKey } from '@metaplex-foundation/umi';
import { publicKey as publicKeySerializer, string } from '@metaplex-foundation/umi/serializers';
const tokenMetadataProgramId = publicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');
const metadata = umi.eddsa.findPda(tokenMetadataProgramId, [
string({ size: 'variable' }).serialize('metadata'),
publicKeySerializer().serialize(tokenMetadataProgramId),
publicKeySerializer().serialize(mint),
]);
Note that in most cases, programs will provide helper methods to find specific PDAs. For instance, the code snippet above can be simplified to the following using the findMetadataPda
method of the @metaplex-foundation/mpl-token-metadata
Kinobi-generated library.
import { findMetadataPda } from '@metaplex-foundation/mpl-token-metadata';
const metadata = findMetadataPda(umi, { mint })
The following helper methods are also available to help manage PDAs.
// Check if the provided value is a Pda.
isPda(myPda);
// Check if the provided public key is on the EdDSA elliptic curve.
umi.eddsa.isOnCurve(myPublicKey);
Signers
A signer is a public key that can sign transactions and messages. This enables transactions to be signed by the required accounts and wallets to prove their identity by signing messages. In Umi, it is represented by the following interface.
interface Signer {
publicKey: PublicKey;
signMessage(message: Uint8Array): Promise<Uint8Array>;
signTransaction(transaction: Transaction): Promise<Transaction>;
signAllTransactions(transactions: Transaction[]): Promise<Transaction[]>;
}
You may generate a new signer cryptographically using the generateSigner
helper method. Under the hood, this method uses the generateKeypair
method of the EdDSA interface as described in the next section.
const mySigner = generateSigner(umi);
The following helper functions can also be used to manage signers.
// Check if the provided value is a Signer.
isSigner(mySigner);
// Deduplicate an array of signers by public key.
uniqueSigners(mySigners);
As mentioned in the Umi interfaces page, the Umi
interface stores two instances of Signer
: The identity
using the app and the payer
paying for transaction and storage fees. Umi provides plugins to quickly assign new signers to these attributes. The signerIdentity
and signerPayer
plugins are available for this purpose. Note that, by default, the signerIdentity
method will also update the payer
attribute since, in most cases, the identity is also the payer.
umi.use(signerIdentity(mySigner));
// Is equivalent to:
umi.identity = mySigner;
umi.payer = mySigner;
umi.use(signerIdentity(mySigner, false));
// Is equivalent to:
umi.identity = mySigner;
umi.use(signerPayer(mySigner));
// Is equivalent to:
umi.payer = mySigner;
You may also use the generatedSignerIdentity
and generatedSignerPayer
plugins to generate a new signer and immediately assign it to the identity
and/or payer
attributes.
umi.use(generatedSignerIdentity());
umi.use(generatedSignerPayer());
In some cases, a library may require a Signer
to be provided but the current environment does not have access to this wallet as a signer. For instance, this can happen if a transaction is being created on the client but will be later on signed on a private server. It's for that reason that Umi provides a createNoopSigner
helper that creates a new signer from the given public key and simply ignores any signing request. It is then your responsibility to ensure that the transaction is signed before being sent to the blockchain.
const mySigner = createNoopSigner(myPublicKey);
Keypairs
Whilst Umi only relies on the Signer
interface to request signatures from a wallet, it also defines a Keypair
type and a KeypairSigner
type that are explicitly aware of their secret key.
type KeypairSigner = Signer & Keypair;
type Keypair = {
publicKey: PublicKey;
secretKey: Uint8Array;
};
The generateKeypair
, createKeypairFromSeed
and createKeypairFromSecretKey
methods of the EdDSA interface can be used to generate new Keypair
objects.
// Generate a new random keypair.
const myKeypair = umi.eddsa.generateKeypair();
// Restore a keypair using a seed.
const myKeypair = umi.eddsa.createKeypairFromSeed(mySeed);
// Restore a keypair using its secret key.
const myKeypair = umi.eddsa.createKeypairFromSecretKey(mySecretKey);
In order to use these keypairs as signers throughout your application, you can use the createSignerFromKeypair
helper method. This method will return an instance of KeypairSigner
to ensure that we can access the secret key when needed.
const myKeypair = umi.eddsa.generateKeypair();
const myKeypairSigner = createSignerFromKeypair(umi, myKeypair);
Note that the code snippet above is equivalent to using the generateSigner
helper method described in the previous section.
Helper functions and plugins also exist to manage keypairs.
// Check if the provided signer is a KeypairSigner object.
isKeypairSigner(mySigner);
// Register a new keypair as the identity and payer.
umi.use(keypairIdentity(myKeypair));
// Register a new keypair as the payer only.
umi.use(keypairPayer(myKeypair));
Signing messages
The Signer
object and the EdDSA interface can be used together to sign and verify messages like so.
const myMessage = utf8.serialize('Hello, world!');
const mySignature = await mySigner.signMessage(myMessage)
const mySignatureIsCorrect = umi.eddsa.verify(myMessage, mySignature, mySigner.publicKey);
Signing transactions
Once we have a Signer
instance, signing a transaction or a set of transactions is as simple as calling the signTransaction
or signAllTransactions
methods.
const mySignedTransaction = await mySigner.signTransaction(myTransaction);
const mySignedTransactions = await mySigner.signAllTransactions(myTransactions);
If you need multiple signers to all sign the same transaction, you may use the signTransaction
helper method like so.
const mySignedTransaction = await signTransaction(myTransaction, mySigners);
Going one step further, if you have multiple transactions that each need to be signed by one or more signers, the signAllTransactions
function can help you with that. It will even ensure that, if a signer is required to sign more than one transaction, it will use the signer.signAllTransactions
method on all of them at once.
// In this example, mySigner2 will sign both transactions
// using the signAllTransactions method.
const mySignedTransactions = await signAllTransactions([
{ transaction: myFirstTransaction, signers: [mySigner1, mySigner2] },
{ transaction: mySecondTransaction, signers: [mySigner2, mySigner3] }
]);
If you are creating a Signer
manually and therefore implementing its signTransaction
method, you may want to use the addTransactionSignature
helper function to add the signature to the transaction. This will ensure the provided signature is required by the transaction and pushed at the right index of the transaction's signatures
array.
const mySignedTransaction = addTransactionSignature(myTransaction, mySignature, myPublicKey);