diff --git a/packages/signers/README.md b/packages/signers/README.md index a8257fbcf..d02123ca0 100644 --- a/packages/signers/README.md +++ b/packages/signers/README.md @@ -326,7 +326,7 @@ const keypairFile = fs.readFileSync('~/.config/solana/id.json'); const keypairBytes = new Uint8Array(JSON.parse(keypairFile.toString())); // Create a KeyPairSigner from the bytes. -const { privateKey, publicKey } = await createKeyPairSignerFromBytes(keypairBytes); +const signer = await createKeyPairSignerFromBytes(keypairBytes); ``` #### `createKeyPairSignerFromPrivateKeyBytes()` diff --git a/packages/signers/src/account-signer-meta.ts b/packages/signers/src/account-signer-meta.ts index 78493db09..a8c825fee 100644 --- a/packages/signers/src/account-signer-meta.ts +++ b/packages/signers/src/account-signer-meta.ts @@ -9,7 +9,31 @@ import { deduplicateSigners } from './deduplicate-signers'; import { ITransactionMessageWithFeePayerSigner } from './fee-payer-signer'; import { isTransactionSigner, TransactionSigner } from './transaction-signer'; -/** An extension of the IAccountMeta type that keeps track of its transaction signer. */ +/** + * An extension of the {@link IAccountMeta} type that allows us to store {@link TransactionSigner | TransactionSigners} inside it. + * + * Note that, because this type represents a signer, it must use one the following two roles: + * - {@link AccountRole.READONLY_SIGNER} + * - {@link AccountRole.WRITABLE_SIGNER} + * + * @typeParam TAddress - Supply a string literal to define an account having a particular address. + * @typeParam TSigner - Optionally provide a narrower type for the {@link TransactionSigner} to use within the account meta. + * + * @interface + * + * @example + * ```ts + * import { AccountRole } from '@solana/instructions'; + * import { generateKeyPairSigner, IAccountSignerMeta } from '@solana/signers'; + * + * const signer = await generateKeyPairSigner(); + * const account: IAccountSignerMeta = { + * address: signer.address, + * role: AccountRole.READONLY_SIGNER, + * signer, + * }; + * ``` + */ export interface IAccountSignerMeta< TAddress extends string = string, TSigner extends TransactionSigner = TransactionSigner, @@ -18,18 +42,78 @@ export interface IAccountSignerMeta< readonly signer: TSigner; } +/** + * A union type that supports base account metas as well as {@link IAccountSignerMeta | signer account metas}. + */ type IAccountMetaWithSigner = | IAccountLookupMeta | IAccountMeta | IAccountSignerMeta; -/** A variation of the instruction type that allows IAccountSignerMeta in its account metas. */ +/** + * Composable type that allows {@link IAccountSignerMeta | IAccountSignerMetas} to be used inside the instruction's `accounts` array + * + * @typeParam TSigner - Optionally provide a narrower type for {@link TransactionSigner | TransactionSigners}. + * @typeParam TAccounts - Optionally provide a narrower type for the account metas. + * + * @interface + * + * @example + * ```ts + * import { AccountRole, IInstruction } from '@solana/instructions'; + * import { generateKeyPairSigner, IInstructionWithSigners } from '@solana/signers'; + * + * const [authority, buffer] = await Promise.all([ + * generateKeyPairSigner(), + * generateKeyPairSigner(), + * ]); + * const instruction: IInstruction & IInstructionWithSigners = { + * programAddress: address('1234..5678'), + * accounts: [ + * // The authority is a signer account. + * { + * address: authority.address, + * role: AccountRole.READONLY_SIGNER, + * signer: authority, + * }, + * // The buffer is a writable account. + * { address: buffer.address, role: AccountRole.WRITABLE }, + * ], + * }; + * ``` + */ export type IInstructionWithSigners< TSigner extends TransactionSigner = TransactionSigner, TAccounts extends readonly IAccountMetaWithSigner[] = readonly IAccountMetaWithSigner[], > = Pick, 'accounts'>; -/** A variation of the transaction message type that allows IAccountSignerMeta in its account metas. */ +/** + * A {@link BaseTransactionMessage} type extension that accept {@link TransactionSigner | TransactionSigners}. + * + * Namely, it allows: + * - a {@link TransactionSigner} to be used as the fee payer and + * - {@link IInstructionWithSigners} to be used in its instructions. + * + * + * @typeParam TAddress - Supply a string literal to define an account having a particular address. + * @typeParam TSigner - Optionally provide a narrower type for {@link TransactionSigner | TransactionSigners}. + * @typeParam TAccounts - Optionally provide a narrower type for the account metas. + * + * @example + * ```ts + * import { IInstruction } from '@solana/instructions'; + * import { BaseTransactionMessage } from '@solana/transaction-messages'; + * import { generateKeyPairSigner, IInstructionWithSigners, ITransactionMessageWithSigners } from '@solana/signers'; + * + * const signer = await generateKeyPairSigner(); + * const firstInstruction: IInstruction = { ... }; + * const secondInstruction: IInstructionWithSigners = { ... }; + * const transactionMessage: BaseTransactionMessage & ITransactionMessageWithSigners = { + * feePayer: signer, + * instructions: [firstInstruction, secondInstruction], + * } + * ``` + */ export type ITransactionMessageWithSigners< TAddress extends string = string, TSigner extends TransactionSigner = TransactionSigner, @@ -40,7 +124,32 @@ export type ITransactionMessageWithSigners< 'instructions' >; -/** Extract all signers from an instruction that may contain IAccountSignerMeta accounts. */ +/** + * Extracts and deduplicates all {@link TransactionSigner | TransactionSigners} stored + * inside the account metas of an {@link IInstructionWithSigners | instruction}. + * + * Any extracted signers that share the same {@link Address} will be de-duplicated. + * + * @typeParam TSigner - Optionally provide a narrower type for {@link TransactionSigner | TransactionSigners}. + * + * @example + * ```ts + * import { IInstructionWithSigners, getSignersFromInstruction } from '@solana/signers'; + * + * const signerA = { address: address('1111..1111'), signTransactions: async () => {} }; + * const signerB = { address: address('2222..2222'), signTransactions: async () => {} }; + * const instructionWithSigners: IInstructionWithSigners = { + * accounts: [ + * { address: signerA.address, signer: signerA, ... }, + * { address: signerB.address, signer: signerB, ... }, + * { address: signerA.address, signer: signerA, ... }, + * ], + * }; + * + * const instructionSigners = getSignersFromInstruction(instructionWithSigners); + * // ^ [signerA, signerB] + * ``` + */ export function getSignersFromInstruction( instruction: IInstructionWithSigners, ): readonly TSigner[] { @@ -49,7 +158,43 @@ export function getSignersFromInstruction {} }; + * const signerB = { address: address('2222..2222'), signTransactions: async () => {} }; + * const firstInstruction: IInstruction & IInstructionWithSigners = { + * programAddress: address('1234..5678'), + * accounts: [{ address: signerA.address, signer: signerA, ... }], + * }; + * const secondInstruction: IInstruction & IInstructionWithSigners = { + * programAddress: address('1234..5678'), + * accounts: [{ address: signerB.address, signer: signerB, ... }], + * }; + * const transactionMessage: ITransactionMessageWithSigners = { + * feePayer: signerA, + * instructions: [firstInstruction, secondInstruction], + * } + * + * const transactionSigners = getSignersFromTransactionMessage(transactionMessage); + * // ^ [signerA, signerB] + * ``` + */ export function getSignersFromTransactionMessage< TAddress extends string = string, TSigner extends TransactionSigner = TransactionSigner, diff --git a/packages/signers/src/add-signers.ts b/packages/signers/src/add-signers.ts index 6ab5ac49a..b86ccf82d 100644 --- a/packages/signers/src/add-signers.ts +++ b/packages/signers/src/add-signers.ts @@ -5,7 +5,41 @@ import { IAccountSignerMeta, IInstructionWithSigners, ITransactionMessageWithSig import { deduplicateSigners } from './deduplicate-signers'; import { TransactionSigner } from './transaction-signer'; -/** Attaches the provided signers to the account metas of an instruction when applicable. */ +/** + * Attaches the provided {@link TransactionSigner | TransactionSigners} to the + * account metas of an instruction when applicable. + * + * For an account meta to match a provided signer it: + * - Must have a signer role ({@link AccountRole.READONLY_SIGNER} or {@link AccountRole.WRITABLE_SIGNER}). + * - Must have the same address as the provided signer. + * - Must not have an attached signer already. + * + * @typeParam TInstruction - The inferred type of the instruction provided. + * + * @example + * ```ts + * import { AccountRole, IInstruction } from '@solana/instructions'; + * import { addSignersToInstruction, TransactionSigner } from '@solana/signers'; + * + * const instruction: IInstruction = { + * accounts: [ + * { address: '1111' as Address, role: AccountRole.READONLY_SIGNER }, + * { address: '2222' as Address, role: AccountRole.WRITABLE_SIGNER }, + * ], + * // ... + * }; + * + * const signerA: TransactionSigner<'1111'>; + * const signerB: TransactionSigner<'2222'>; + * const instructionWithSigners = addSignersToInstruction( + * [signerA, signerB], + * instruction + * ); + * + * // instructionWithSigners.accounts[0].signer === signerA + * // instructionWithSigners.accounts[1].signer === signerB + * ``` + */ export function addSignersToInstruction( signers: TransactionSigner[], instruction: TInstruction | (IInstructionWithSigners & TInstruction), @@ -27,7 +61,47 @@ export function addSignersToInstruction( }); } -/** Attaches the provided signers to the account metas of a transaction message when applicable. */ +/** + * Attaches the provided {@link TransactionSigner | TransactionSigners} to the + * account metas of all instructions inside a transaction message, when applicable. + * + * For an account meta to match a provided signer it: + * - Must have a signer role ({@link AccountRole.READONLY_SIGNER} or {@link AccountRole.WRITABLE_SIGNER}). + * - Must have the same address as the provided signer. + * - Must not have an attached signer already. + * + * @typeParam TTransactionMessage - The inferred type of the transaction message provided. + * + * @example + * ```ts + * import { AccountRole, IInstruction } from '@solana/instructions'; + * import { BaseTransactionMessage } from '@solana/transaction-messages'; + * import { addSignersToTransactionMessage, TransactionSigner } from '@solana/signers'; + * + * const instructionA: IInstruction = { + * accounts: [{ address: '1111' as Address, role: AccountRole.READONLY_SIGNER }], + * // ... + * }; + * const instructionB: IInstruction = { + * accounts: [{ address: '2222' as Address, role: AccountRole.WRITABLE_SIGNER }], + * // ... + * }; + * const transactionMessage: BaseTransactionMessage = { + * instructions: [instructionA, instructionB], + * // ... + * } + * + * const signerA: TransactionSigner<'1111'>; + * const signerB: TransactionSigner<'2222'>; + * const transactionMessageWithSigners = addSignersToTransactionMessage( + * [signerA, signerB], + * transactionMessage + * ); + * + * // transactionMessageWithSigners.instructions[0].accounts[0].signer === signerA + * // transactionMessageWithSigners.instructions[1].accounts[0].signer === signerB + * ``` + */ export function addSignersToTransactionMessage( signers: TransactionSigner[], transactionMessage: TTransactionMessage | (ITransactionMessageWithSigners & TTransactionMessage), diff --git a/packages/signers/src/deduplicate-signers.ts b/packages/signers/src/deduplicate-signers.ts index f0d838500..af490c053 100644 --- a/packages/signers/src/deduplicate-signers.ts +++ b/packages/signers/src/deduplicate-signers.ts @@ -4,7 +4,13 @@ import { SOLANA_ERROR__SIGNER__ADDRESS_CANNOT_HAVE_MULTIPLE_SIGNERS, SolanaError import { MessageSigner } from './message-signer'; import { TransactionSigner } from './transaction-signer'; -/** Removes all duplicated signers from a provided array by comparing their addresses. */ +/** + * Removes all duplicated {@link MessageSigner | MessageSigners} and + * {@link TransactionSigner | TransactionSigners} from a provided array + * by comparing their {@link Address | addresses}. + * + * @internal + */ export function deduplicateSigners( signers: readonly TSigner[], ): readonly TSigner[] { diff --git a/packages/signers/src/fee-payer-signer.ts b/packages/signers/src/fee-payer-signer.ts index 16cd076f4..a459d527b 100644 --- a/packages/signers/src/fee-payer-signer.ts +++ b/packages/signers/src/fee-payer-signer.ts @@ -2,6 +2,24 @@ import { BaseTransactionMessage, ITransactionMessageWithFeePayer } from '@solana import { TransactionSigner } from './transaction-signer'; +/** + * Alternative to {@link ITransactionMessageWithFeePayer} that uses a {@link TransactionSigner} for the fee payer. + * + * @typeParam TAddress - Supply a string literal to define a fee payer having a particular address. + * @typeParam TSigner - Optionally provide a narrower type for the {@link TransactionSigner}. + * + * @example + * ```ts + * import { BaseTransactionMessage } from '@solana/transaction-messages'; + * import { generateKeyPairSigner, ITransactionMessageWithFeePayerSigner } from '@solana/signers'; + * + * const transactionMessage: BaseTransactionMessage & ITransactionMessageWithFeePayerSigner = { + * feePayer: await generateKeyPairSigner(), + * instructions: [], + * version: 0, + * }; + * ``` + */ export interface ITransactionMessageWithFeePayerSigner< TAddress extends string = string, TSigner extends TransactionSigner = TransactionSigner, @@ -9,6 +27,26 @@ export interface ITransactionMessageWithFeePayerSigner< readonly feePayer: TSigner; } +/** + * Sets the fee payer of a {@link BaseTransactionMessage | transaction message} + * using a {@link TransactionSigner}. + * + * @typeParam TFeePayerAddress - Supply a string literal to define a fee payer having a particular address. + * @typeParam TTransactionMessage - The inferred type of the transaction message provided. + * + * @example + * ```ts + * import { pipe } from '@solana/functional'; + * import { generateKeyPairSigner, setTransactionMessageFeePayerSigner } from '@solana/signers'; + * import { createTransactionMessage } from '@solana/transaction-messages'; + * + * const feePayer = await generateKeyPairSigner(); + * const transactionMessage = pipe( + * createTransactionMessage({ version: 0 }), + * message => setTransactionMessageFeePayerSigner(signer, message), + * ); + * ``` + */ export function setTransactionMessageFeePayerSigner< TFeePayerAddress extends string, TTransactionMessage extends BaseTransactionMessage & diff --git a/packages/signers/src/index.ts b/packages/signers/src/index.ts index bbb474dda..8cd136806 100644 --- a/packages/signers/src/index.ts +++ b/packages/signers/src/index.ts @@ -1,3 +1,79 @@ +/** + * This package provides an abstraction layer over signing messages and transactions in Solana. + * It can be used standalone, but it is also exported as part of the Solana JavaScript SDK + * [`@solana/web3.js@next`](https://github.com/anza-xyz/solana-web3.js/tree/main/packages/library). + * + * You can think of signers as an abstract way to sign messages and transactions. + * This could be using a {@link CryptoKeyPair}, a wallet adapter in the browser, + * a Noop signer for testing purposes, or anything you want. + * Here's an example using a {@link CryptoKeyPair} signer: + * + * @example + * ```ts + * import { pipe } from '@solana/functional'; + * import { generateKeyPairSigner } from '@solana/signers'; + * import { createTransactionMessage } from '@solana/transaction-messages'; + * import { compileTransaction } from '@solana/transactions'; + * + * // Generate a key pair signer. + * const mySigner = await generateKeyPairSigner(); + * mySigner.address; // Address; + * + * // Sign one or multiple messages. + * const myMessage = createSignableMessage('Hello world!'); + * const [messageSignatures] = await mySigner.signMessages([myMessage]); + * + * // Sign one or multiple transaction messages. + * const myTransactionMessage = pipe( + * createTransactionMessage({ version: 0 }), + * // Add instructions, fee payer, lifetime, etc. + * ); + * const myTransaction = compileTransaction(myTransactionMessage); + * const [transactionSignatures] = await mySigner.signTransactions([myTransaction]); + * ``` + * + * As you can see, this provides a consistent API regardless of how things are being signed + * behind the scenes. If tomorrow we need to use a browser wallet instead, we'd simply + * need to swap the {@link generateKeyPairSigner} function with the signer factory of our choice. + * + * @remarks + * This package offers a total of five different types of signers that may be used in combination when applicable. + * Three of them allow us to sign transactions whereas the other two are used for regular message signing. + * + * They are separated into three categories: + * + * - **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. + * + * Thus, we end up with the following interfaces. + * + * | | Partial signers | Modifying signers | Sending signers | + * | ------------------- | -------------------------- | ---------------------------- | -------------------------- | + * | {@link TransactionSigner} | {@link TransactionPartialSigner} | {@link TransactionModifyingSigner} | {@link TransactionSendingSigner} | + * | {@link MessageSigner} | {@link MessagePartialSigner} | {@link MessageModifyingSigner} | N/A | + * + * This package also provides the following concrete signer implementations: + * + * - The {@link KeyPairSigner} which uses a {@link CryptoKeyPair} to sign messages and transactions. + * - The {@link NoopSigner} which does not sign anything and is mostly useful for testing purposes + * or for indicating that an account will be signed in a different environment (e.g. sending a + * transaction to your server so it can sign it). + * + * Additionally, this package allows {@link TransactionSigner | TransactionSigners} to be stored + * inside the account meta of an instruction. This allows us to create instructions by passing + * around signers instead of addresses when applicable which, in turn, allows us to + * {@link signTransactionMessageWithSigners | sign an entire transaction automatically} + * without having to scan through its instructions to find the required signers. + * + * @packageDocumentation + */ export * from './account-signer-meta'; export * from './add-signers'; export * from './fee-payer-signer'; diff --git a/packages/signers/src/keypair-signer.ts b/packages/signers/src/keypair-signer.ts index 80ab07ffc..8595271ae 100644 --- a/packages/signers/src/keypair-signer.ts +++ b/packages/signers/src/keypair-signer.ts @@ -7,11 +7,50 @@ import { partiallySignTransaction } from '@solana/transactions'; import { isMessagePartialSigner, MessagePartialSigner } from './message-partial-signer'; import { isTransactionPartialSigner, TransactionPartialSigner } from './transaction-partial-signer'; -/** Defines a signer capable of signing messages and transactions using a CryptoKeyPair. */ +/** + * Defines a signer that uses a {@link CryptoKeyPair} to sign messages and transactions. + * + * It implements both the {@link MessagePartialSigner} and {@link TransactionPartialSigner} + * interfaces and keeps track of the {@link CryptoKeyPair} instance used + * to sign messages and transactions. + * + * @typeParam TAddress - Supply a string literal to define a signer having a particular address. + * + * @example + * ```ts + * import { generateKeyPairSigner } from '@solana/signers'; + * + * const signer = generateKeyPairSigner(); + * signer.address; // Address; + * signer.keyPair; // CryptoKeyPair; + * const [messageSignatures] = await signer.signMessages([message]); + * const [transactionSignatures] = await signer.signTransactions([transaction]); + * ``` + * + * @see {@link generateKeyPairSigner} + * @see {@link createSignerFromKeyPair} + * @see {@link createKeyPairSignerFromBytes} + * @see {@link createKeyPairSignerFromPrivateKeyBytes} + * @see {@link isKeyPairSigner} + * @see {@link assertIsKeyPairSigner} + */ export type KeyPairSigner = MessagePartialSigner & TransactionPartialSigner & { keyPair: CryptoKeyPair }; -/** Checks whether the provided value implements the {@link KeyPairSigner} interface. */ +/** + * Checks whether the provided value implements the {@link KeyPairSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { generateKeyPairSigner, isKeyPairSigner } from '@solana/signers'; + * + * const signer = await generateKeyPairSigner(); + * isKeyPairSigner(signer); // true + * isKeyPairSigner({ address: address('1234..5678') }); // false + * ``` + */ export function isKeyPairSigner(value: { [key: string]: unknown; address: Address; @@ -24,7 +63,20 @@ export function isKeyPairSigner(value: { ); } -/** Asserts that the provided value implements the {@link KeyPairSigner} interface. */ +/** + * Asserts that the provided value implements the {@link KeyPairSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { generateKeyPairSigner, assertIsKeyPairSigner } from '@solana/signers'; + * + * const signer = await generateKeyPairSigner(); + * assertIsKeyPairSigner(signer); // void + * assertIsKeyPairSigner({ address: address('1234..5678') }); // Throws an error. + * ``` + */ export function assertIsKeyPairSigner(value: { [key: string]: unknown; address: Address; @@ -36,7 +88,27 @@ export function assertIsKeyPairSigner(value: { } } -/** Creates a KeyPairSigner from the provided Crypto KeyPair. */ +/** + * Creates a {@link KeyPairSigner} from a provided {@link CryptoKeyPair}. + * + * The {@link MessagePartialSigner#signMessages | signMessages} and + * {@link TransactionPartialSigner#signTransactions | signTransactions} + * functions of the returned signer will use the private key of the provided + * key pair to sign messages and transactions. + * + * Note that both the {@link MessagePartialSigner#signMessages | signMessages} and + * {@link TransactionPartialSigner#signTransactions | signTransactions} implementations + * are parallelized, meaning that they will sign all provided messages and transactions in parallel. + * + * @example + * ```ts + * import { generateKeyPair } from '@solana/keys'; + * import { createSignerFromKeyPair, KeyPairSigner } from '@solana/signers'; + * + * const keyPair: CryptoKeyPair = await generateKeyPair(); + * const signer: KeyPairSigner = await createSignerFromKeyPair(keyPair); + * ``` + */ export async function createSignerFromKeyPair(keyPair: CryptoKeyPair): Promise { const address = await getAddressFromPublicKey(keyPair.publicKey); const out: KeyPairSigner = { @@ -61,12 +133,41 @@ export async function createSignerFromKeyPair(keyPair: CryptoKeyPair): Promise { return await createSignerFromKeyPair(await generateKeyPair()); } -/** Creates a signer capable of signing messages and transactions using the 64 bytes of a KeyPair. */ +/** + * Creates a new {@link KeyPairSigner} from a 64-bytes `Uint8Array` secret key (private key and public key). + * + * @example + * ```ts + * import fs from 'fs'; + * import { createKeyPairSignerFromBytes } from '@solana/signers'; + * + * // Get bytes from local keypair file. + * const keypairFile = fs.readFileSync('~/.config/solana/id.json'); + * const keypairBytes = new Uint8Array(JSON.parse(keypairFile.toString())); + * + * // Create a KeyPairSigner from the bytes. + * const signer = await createKeyPairSignerFromBytes(keypairBytes); + * ``` + * + * @see {@link createKeyPairSignerFromPrivateKeyBytes} if you only have the 32-bytes private key instead. + */ export async function createKeyPairSignerFromBytes( bytes: ReadonlyUint8Array, extractable?: boolean, @@ -74,7 +175,22 @@ export async function createKeyPairSignerFromBytes( return await createSignerFromKeyPair(await createKeyPairFromBytes(bytes, extractable)); } -/** Creates a signer capable of signing messages and transactions using the 32 bytes of a private key. */ +/** + * Creates a new {@link KeyPairSigner} from a 32-bytes `Uint8Array` private key. + * + * @example + * ```ts + * import { getUtf8Encoder } from '@solana/codecs-strings'; + * import { createKeyPairSignerFromPrivateKeyBytes } from '@solana/signers'; + * + * const message = getUtf8Encoder().encode('Hello, World!'); + * const seed = new Uint8Array(await crypto.subtle.digest('SHA-256', message)); + * + * const derivedSigner = await createKeyPairSignerFromPrivateKeyBytes(seed); + * ``` + * + * @see {@link createKeyPairSignerFromBytes} if you have the 64-bytes secret key instead (private key and public key). + */ export async function createKeyPairSignerFromPrivateKeyBytes( bytes: ReadonlyUint8Array, extractable?: boolean, diff --git a/packages/signers/src/message-modifying-signer.ts b/packages/signers/src/message-modifying-signer.ts index 6f2e02161..a315cdc7b 100644 --- a/packages/signers/src/message-modifying-signer.ts +++ b/packages/signers/src/message-modifying-signer.ts @@ -4,9 +4,54 @@ import { SOLANA_ERROR__SIGNER__EXPECTED_MESSAGE_MODIFYING_SIGNER, SolanaError } import { SignableMessage } from './signable-message'; import { BaseSignerConfig } from './types'; +/** + * The configuration to optionally provide when calling the + * {@link MessageModifyingSigner#modifyAndSignMessages | modifyAndSignMessages} method. + * + * @see {@link BaseSignerConfig} + */ export type MessageModifyingSignerConfig = BaseSignerConfig; -/** Defines a signer capable of signing messages. */ +/** + * A signer interface that _potentially_ modifies the content + * of the provided {@link SignableMessage | SignableMessages} before signing them. + * + * For instance, this enables wallets to prefix or suffix nonces to the messages they sign. + * For each message, instead of returning a {@link SignatureDirectory}, the + * {@link MessageModifyingSigner#modifyAndSignMessages | modifyAndSignMessages} function + * returns an updated {@link SignableMessage} with a potentially modified content and signature dictionary. + * + * @typeParam TAddress - Supply a string literal to define a signer having a particular address. + * + * @example + * ```ts + * const signer: MessageModifyingSigner<'1234..5678'> = { + * address: address('1234..5678'), + * modifyAndSignMessages: async ( + * messages: SignableMessage[] + * ): Promise => { + * // My custom signing logic. + * }, + * }; + * ``` + * + * @remarks + * Here are the main characteristics of this signer interface: + * + * - **Sequential**. Contrary to partial signers, these cannot be executed in + * parallel as each call can modify the content of the message. + * - **First signers**. For a given message, a modifying signer must always be used + * before a partial signer as the former will likely modify the message and + * thus impact the outcome of the latter. + * - **Potential conflicts**. If more than one modifying signer is provided, the second + * signer may invalidate the signature of the first one. However, modifying signers + * may decide not to modify a message based on the existence of signatures for that message. + * + * @see {@link SignableMessage} + * @see {@link createSignableMessage} + * @see {@link isMessageModifyingSigner} + * @see {@link assertIsMessageModifyingSigner} + */ export type MessageModifyingSigner = Readonly<{ address: Address; modifyAndSignMessages( @@ -15,7 +60,23 @@ export type MessageModifyingSigner = Readonly< ): Promise; }>; -/** Checks whether the provided value implements the {@link MessageModifyingSigner} interface. */ +/** + * Checks whether the provided value implements the {@link MessageModifyingSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { isMessageModifyingSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * isMessageModifyingSigner({ address, modifyAndSignMessages: async () => {} }); // true + * isMessageModifyingSigner({ address }); // false + * ``` + * + * @see {@link assertIsMessageModifyingSigner} + */ export function isMessageModifyingSigner(value: { [key: string]: unknown; address: Address; @@ -27,7 +88,23 @@ export function isMessageModifyingSigner(value: { ); } -/** Asserts that the provided value implements the {@link MessageModifyingSigner} interface. */ +/** + * Asserts that the provided value implements the {@link MessageModifyingSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { assertIsMessageModifyingSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * assertIsMessageModifyingSigner({ address, modifyAndSignMessages: async () => {} }); // void + * assertIsMessageModifyingSigner({ address }); // Throws an error. + * ``` + * + * @see {@link isMessageModifyingSigner} + */ export function assertIsMessageModifyingSigner(value: { [key: string]: unknown; address: Address; diff --git a/packages/signers/src/message-partial-signer.ts b/packages/signers/src/message-partial-signer.ts index c2965e142..3f0c5698f 100644 --- a/packages/signers/src/message-partial-signer.ts +++ b/packages/signers/src/message-partial-signer.ts @@ -4,9 +4,49 @@ import { SOLANA_ERROR__SIGNER__EXPECTED_MESSAGE_PARTIAL_SIGNER, SolanaError } fr import { SignableMessage } from './signable-message'; import { BaseSignerConfig, SignatureDictionary } from './types'; +/** + * The configuration to optionally provide when calling the + * {@link MessagePartialSigner#signMessages | signMessages} method. + * + * @see {@link BaseSignerConfig} + */ export type MessagePartialSignerConfig = BaseSignerConfig; -/** Defines a signer capable of signing messages. */ +/** + * A signer interface that signs an array of {@link SignableMessage | SignableMessages} + * without modifying their content. + * + * It defines a {@link MessagePartialSigner#signMessages | signMessages} function + * that returns a {@link SignatureDictionary} for each provided message. + * Such signature dictionaries are expected to be merged with the existing ones if any. + * + * @typeParam TAddress - Supply a string literal to define a signer having a particular address. + * + * @example + * ```ts + * const signer: MessagePartialSigner<'1234..5678'> = { + * address: address('1234..5678'), + * signMessages: async ( + * messages: SignableMessage[] + * ): Promise => { + * // My custom signing logic. + * }, + * }; + * ``` + * + * @remarks + * Here are the main characteristics of this signer interface: + * + * - **Parallel**. When multiple signers sign the same message, we can + * perform this operation in parallel to obtain all their signatures. + * - **Flexible order**. The order in which we use these signers + * for a given message doesn’t matter. + * + * @see {@link SignableMessage} + * @see {@link createSignableMessage} + * @see {@link isMessagePartialSigner} + * @see {@link assertIsMessagePartialSigner} + */ export type MessagePartialSigner = Readonly<{ address: Address; signMessages( @@ -15,7 +55,23 @@ export type MessagePartialSigner = Readonly<{ ): Promise; }>; -/** Checks whether the provided value implements the {@link MessagePartialSigner} interface. */ +/** + * Checks whether the provided value implements the {@link MessagePartialSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { isMessagePartialSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * isMessagePartialSigner({ address, signMessages: async () => {} }); // true + * isMessagePartialSigner({ address }); // false + * ``` + * + * @see {@link assertIsMessagePartialSigner} + */ export function isMessagePartialSigner(value: { [key: string]: unknown; address: Address; @@ -23,7 +79,23 @@ export function isMessagePartialSigner(value: { return 'signMessages' in value && typeof value.signMessages === 'function'; } -/** Asserts that the provided value implements the {@link MessagePartialSigner} interface. */ +/** + * Asserts that the provided value implements the {@link MessagePartialSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { assertIsMessagePartialSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * assertIsMessagePartialSigner({ address, signMessages: async () => {} }); // void + * assertIsMessagePartialSigner({ address }); // Throws an error. + * ``` + * + * @see {@link isMessagePartialSigner} + */ export function assertIsMessagePartialSigner(value: { [key: string]: unknown; address: Address; diff --git a/packages/signers/src/message-signer.ts b/packages/signers/src/message-signer.ts index cde33c027..1dee98a1d 100644 --- a/packages/signers/src/message-signer.ts +++ b/packages/signers/src/message-signer.ts @@ -4,12 +4,36 @@ import { SOLANA_ERROR__SIGNER__EXPECTED_MESSAGE_SIGNER, SolanaError } from '@sol import { isMessageModifyingSigner, MessageModifyingSigner } from './message-modifying-signer'; import { isMessagePartialSigner, MessagePartialSigner } from './message-partial-signer'; -/** Defines a signer capable of signing messages. */ +/** + * Defines a signer capable of signing messages. + * + * @see {@link MessageModifyingSigner} For signers that can modify messages before signing them. + * @see {@link MessagePartialSigner} For signers that can be used in parallel. + * @see {@link isMessageSigner} + * @see {@link assertIsMessageSigner} + */ export type MessageSigner = | MessageModifyingSigner | MessagePartialSigner; -/** Checks whether the provided value implements the {@link MessageSigner} interface. */ +/** + * Checks whether the provided value implements the {@link MessageSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { isMessageSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * isMessageSigner({ address, signMessages: async () => {} }); // true + * isMessageSigner({ address, modifyAndSignMessages: async () => {} }); // true + * isMessageSigner({ address }); // false + * ``` + * + * @see {@link assertIsMessageSigner} + */ export function isMessageSigner(value: { [key: string]: unknown; address: Address; @@ -17,7 +41,24 @@ export function isMessageSigner(value: { return isMessagePartialSigner(value) || isMessageModifyingSigner(value); } -/** Asserts that the provided value implements the {@link MessageSigner} interface. */ +/** + * Asserts that the provided value implements the {@link MessageSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { assertIsMessageSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * assertIsMessageSigner({ address, signMessages: async () => {} }); // void + * assertIsMessageSigner({ address, modifyAndSignMessages: async () => {} }); // void + * assertIsMessageSigner({ address }); // Throws an error. + * ``` + * + * @see {@link isMessageSigner} + */ export function assertIsMessageSigner(value: { [key: string]: unknown; address: Address; diff --git a/packages/signers/src/noop-signer.ts b/packages/signers/src/noop-signer.ts index 0c743fb72..9d93a3d21 100644 --- a/packages/signers/src/noop-signer.ts +++ b/packages/signers/src/noop-signer.ts @@ -3,13 +3,55 @@ import { Address } from '@solana/addresses'; import { MessagePartialSigner } from './message-partial-signer'; import { TransactionPartialSigner } from './transaction-partial-signer'; -/** Defines a no-operation signer that pretends to partially sign messages and transactions. */ +/** + * Defines a Noop (No-Operation) signer that pretends to partially sign messages and transactions. + * + * For a given {@link Address}, a Noop Signer can be created to offer an implementation of both + * the {@link MessagePartialSigner} and {@link TransactionPartialSigner} interfaces such that + * they do not sign anything. Namely, signing a transaction or a message with a `NoopSigner` + * will return an empty `SignatureDictionary`. + * + * @typeParam TAddress - Supply a string literal to define a Noop signer having a particular address. + * + * @example + * ```ts + * import { address } from '@solana/addresses'; + * import { createNoopSigner } from '@solana/signers'; + * + * const signer = createNoopSigner(address('1234..5678')); + * const [messageSignatures] = await signer.signMessages([message]); + * const [transactionSignatures] = await signer.signTransactions([transaction]); + * // ^ Both messageSignatures and transactionSignatures are empty. + * ``` + * + * @remarks + * This signer may be useful: + * + * - For testing purposes. + * - For indicating that a given account is a signer and taking the responsibility to provide + * the signature for that account ourselves. For instance, if we need to send the transaction + * to a server that will sign it and send it for us. + * + * @see {@link createNoopSigner} + */ export type NoopSigner = MessagePartialSigner & TransactionPartialSigner; -/** Creates a NoopSigner from the provided Address. */ -export function createNoopSigner(address: Address): NoopSigner { - const out: NoopSigner = { +/** + * Creates a {@link NoopSigner} from the provided {@link Address}. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { address } from '@solana/addresses'; + * import { createNoopSigner } from '@solana/signers'; + * + * const signer = createNoopSigner(address('1234..5678')); + * ``` + */ +export function createNoopSigner(address: Address): NoopSigner { + const out: NoopSigner = { address, signMessages: messages => Promise.resolve(messages.map(() => Object.freeze({}))), signTransactions: transactions => Promise.resolve(transactions.map(() => Object.freeze({}))), diff --git a/packages/signers/src/sign-transaction.ts b/packages/signers/src/sign-transaction.ts index 0c6c788c4..39b289d7e 100644 --- a/packages/signers/src/sign-transaction.ts +++ b/packages/signers/src/sign-transaction.ts @@ -38,9 +38,38 @@ import { assertIsTransactionMessageWithSingleSendingSigner } from './transaction type CompilableTransactionMessageWithSigners = CompilableTransactionMessage & ITransactionMessageWithSigners; /** - * Signs a transaction using any signers that may be stored in IAccountSignerMeta instruction accounts - * as well as any signers provided explicitly to this function. - * It will ignore TransactionSendingSigners since this function does not send the transaction. + * Extracts all {@link TransactionSigner | TransactionSigners} inside the provided + * transaction message and uses them to return a signed transaction. + * + * It first uses all {@link TransactionModifyingSigner | TransactionModifyingSigners} sequentially before + * using all {@link TransactionPartialSigner | TransactionPartialSigners} in parallel. + * + * If a composite signer implements both interfaces, it will be used as a + * {@link TransactionModifyingSigner} if no other signer implements that interface. + * Otherwise, it will be used as a {@link TransactionPartialSigner}. + * + * @typeParam TTransactionMessage - The inferred type of the transaction message provided. + * + * @example + * ```ts + * const signedTransaction = await partiallySignTransactionMessageWithSigners(transactionMessage); + * ``` + * + * It also accepts an optional {@link AbortSignal} that will be propagated to all signers. + * + * ```ts + * const signedTransaction = await partiallySignTransactionMessageWithSigners(transactionMessage, { + * abortSignal: myAbortController.signal, + * }); + * ``` + * + * @remarks + * Finally, note that this function ignores {@link TransactionSendingSigner | TransactionSendingSigners} + * as it does not send the transaction. Check out the {@link signAndSendTransactionMessageWithSigners} + * function for more details on how to use sending signers. + * + * @see {@link signTransactionMessageWithSigners} + * @see {@link signAndSendTransactionMessageWithSigners} */ export async function partiallySignTransactionMessageWithSigners< TTransactionMessage extends CompilableTransactionMessageWithSigners & @@ -87,10 +116,30 @@ export async function partiallySignTransactionMessageWithSigners< } /** - * Signs a transaction using any signers that may be stored in IAccountSignerMeta instruction accounts - * as well as any signers provided explicitly to this function. - * It will assert that the transaction is fully signed before returning. - * It will ignore TransactionSendingSigners since this function does not send the transaction. + * Extracts all {@link TransactionSigner | TransactionSigners} inside the provided + * transaction message and uses them to return a signed transaction before asserting + * that all signatures required by the transaction are present. + * + * This function delegates to the {@link partiallySignTransactionMessageWithSigners} function + * in order to extract signers from the transaction message and sign the transaction. + * + * @typeParam TTransactionMessage - The inferred type of the transaction message provided. + * + * @example + * ```ts + * const mySignedTransaction = await signTransactionMessageWithSigners(myTransactionMessage); + * + * // With additional config. + * const mySignedTransaction = await signTransactionMessageWithSigners(myTransactionMessage, { + * abortSignal: myAbortController.signal, + * }); + * + * // We now know the transaction is fully signed. + * mySignedTransaction satisfies IFullySignedTransaction; + * ``` + * + * @see {@link partiallySignTransactionMessageWithSigners} + * @see {@link signAndSendTransactionMessageWithSigners} */ export async function signTransactionMessageWithSigners< TTransactionMessage extends CompilableTransactionMessageWithSigners & @@ -129,10 +178,54 @@ export async function signTransactionMessageWithSigners< } /** - * Signs and sends a transaction using any signers that may be stored in IAccountSignerMeta - * instruction accounts as well as any signers provided explicitly to this function. - * It will identify a single TransactionSendingSigners to use for sending the transaction, if any. - * Otherwise, it will send the transaction using the provided fallbackSender. + * Extracts all {@link TransactionSigner | TransactionSigners} inside the provided + * transaction message and uses them to sign it before sending it immediately to the blockchain. + * + * It returns the signature of the sent transaction (i.e. its identifier) as bytes. + * + * @typeParam TTransactionMessage - The inferred type of the transaction message provided. + * + * @example + * ```ts + * import { signAndSendTransactionMessageWithSigners } from '@solana/signers'; + * + * const transactionSignature = await signAndSendTransactionMessageWithSigners(transactionMessage); + * + * // With additional config. + * const transactionSignature = await signAndSendTransactionMessageWithSigners(transactionMessage, { + * abortSignal: myAbortController.signal, + * }); + * ``` + * + * @remarks + * Similarly to the {@link partiallySignTransactionMessageWithSigners} function, it first uses all + * {@link TransactionModifyingSigner | TransactionModifyingSigners} sequentially before using all + * {@link TransactionPartialSigner | TransactionPartialSigners} in parallel. + * It then sends the transaction using the {@link TransactionSendingSigner} it identified. + * + * Composite transaction signers are treated such that at least one sending signer is used if any. + * When a {@link TransactionSigner} implements more than one interface, we use it as a: + * + * - {@link TransactionSendingSigner}, if no other {@link TransactionSendingSigner} exists. + * - {@link TransactionModifyingSigner}, if no other {@link TransactionModifyingSigner} exists. + * - {@link TransactionPartialSigner}, otherwise. + * + * The provided transaction must contain exactly one {@link TransactionSendingSigner} inside its account metas. + * If more than one composite signers implement the {@link TransactionSendingSigner} interface, + * one of them will be selected as the sending signer. Otherwise, if multiple + * {@link TransactionSendingSigner | TransactionSendingSigners} must be selected, the function will throw an error. + * + * If you'd like to assert that a transaction makes use of exactly one {@link TransactionSendingSigner} + * _before_ calling this function, you may use the {@link assertIsTransactionMessageWithSingleSendingSigner} function. + * + * Alternatively, you may use the {@link isTransactionMessageWithSingleSendingSigner} function to provide a + * fallback in case the transaction does not contain any sending signer. + * + * @see {@link assertIsTransactionMessageWithSingleSendingSigner} + * @see {@link isTransactionMessageWithSingleSendingSigner} + * @see {@link partiallySignTransactionMessageWithSigners} + * @see {@link signTransactionMessageWithSigners} + * */ export async function signAndSendTransactionMessageWithSigners< TTransactionMessage extends CompilableTransactionMessageWithSigners = CompilableTransactionMessageWithSigners, diff --git a/packages/signers/src/signable-message.ts b/packages/signers/src/signable-message.ts index 5f41e185d..84573d90b 100644 --- a/packages/signers/src/signable-message.ts +++ b/packages/signers/src/signable-message.ts @@ -2,15 +2,45 @@ import { TextEncoder } from '@solana/text-encoding-impl'; import { SignatureDictionary } from './types'; -/** Defines a message that needs signing and its current set of signatures if any. */ +/** + * Defines a message that needs signing and its current set of signatures if any. + * + * This interface allows {@link MessageModifyingSigner | MessageModifyingSigners} + * to decide on whether or not they should modify the provided message depending + * on whether or not signatures already exist for such message. + * + * It also helps create a more consistent API by providing a structure analogous + * to transactions which also keep track of their {@link SignatureDictionary}. + * + * @example + * ```ts + * import { createSignableMessage } from '@solana/signers'; + * + * const message = createSignableMessage(new Uint8Array([1, 2, 3])); + * message.content; // The content of the message as bytes. + * message.signatures; // The current set of signatures for this message. + * ``` + * + * @see {@link createSignableMessage} + */ export type SignableMessage = Readonly<{ content: Uint8Array; signatures: SignatureDictionary; }>; /** - * Creates a signable message from a provided content. - * If a string is provided, it will be UTF-8 encoded. + * Creates a {@link SignableMessage} from a `Uint8Array` or a UTF-8 string. + * + * It optionally accepts a signature dictionary if the message already contains signatures. + * + * @example + * ```ts + * const message = createSignableMessage(new Uint8Array([1, 2, 3])); + * const messageFromText = createSignableMessage('Hello world!'); + * const messageWithSignatures = createSignableMessage('Hello world!', { + * '1234..5678': new Uint8Array([1, 2, 3]), + * }); + * ``` */ export function createSignableMessage( content: Uint8Array | string, diff --git a/packages/signers/src/transaction-modifying-signer.ts b/packages/signers/src/transaction-modifying-signer.ts index 2fede577a..9a6d0968c 100644 --- a/packages/signers/src/transaction-modifying-signer.ts +++ b/packages/signers/src/transaction-modifying-signer.ts @@ -4,9 +4,54 @@ import { Transaction } from '@solana/transactions'; import { BaseTransactionSignerConfig } from './types'; +/** + * The configuration to optionally provide when calling the + * {@link TransactionModifyingSigner#modifyAndSignTransactions | modifyAndSignTransactions} method. + * + * @see {@link BaseTransactionSignerConfig} + */ export type TransactionModifyingSignerConfig = BaseTransactionSignerConfig; -/** Defines a signer capable of signing transactions. */ +/** + * A signer interface that potentially modifies the provided {@link Transaction | Transactions} + * before signing them. + * + * For instance, this enables wallets to inject additional instructions into the + * transaction before signing them. For each transaction, instead of returning a {@link SignatureDirectory}, + * its {@link TransactionModifyingSigner#modifyAndSignTransactions | modifyAndSignTransactions} + * function returns an updated {@link Transaction} with a potentially + * modified set of instructions and signature dictionary. + * + * @typeParam TAddress - Supply a string literal to define a signer having a particular address. + * + * @example + * ```ts + * const signer: TransactionModifyingSigner<'1234..5678'> = { + * address: address('1234..5678'), + * modifyAndSignTransactions: async ( + * transactions: T[] + * ): Promise => { + * // My custom signing logic. + * }, + * }; + * ``` + * + * @remarks + * Here are the main characteristics of this signer interface: + * + * - **Sequential**. Contrary to partial signers, these cannot be executed in + * parallel as each call can modify the provided transactions. + * - **First signers**. For a given transaction, a modifying signer must always + * be used before a partial signer as the former will likely modify the + * transaction and thus impact the outcome of the latter. + * - **Potential conflicts**. If more than one modifying signer is provided, + * the second signer may invalidate the signature of the first one. However, + * modifying signers may decide not to modify a transaction based on the + * existence of signatures for that transaction. + * + * @see {@link isTransactionModifyingSigner} + * @see {@link assertIsTransactionModifyingSigner} + */ export type TransactionModifyingSigner = Readonly<{ address: Address; modifyAndSignTransactions( @@ -15,7 +60,23 @@ export type TransactionModifyingSigner = Reado ): Promise; }>; -/** Checks whether the provided value implements the {@link TransactionModifyingSigner} interface. */ +/** + * Checks whether the provided value implements the {@link TransactionModifyingSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { isTransactionModifyingSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * isTransactionModifyingSigner({ address, modifyAndSignTransactions: async () => {} }); // true + * isTransactionModifyingSigner({ address }); // false + * ``` + * + * @see {@link assertIsTransactionModifyingSigner} + */ export function isTransactionModifyingSigner(value: { [key: string]: unknown; address: Address; @@ -23,7 +84,23 @@ export function isTransactionModifyingSigner(value: { return 'modifyAndSignTransactions' in value && typeof value.modifyAndSignTransactions === 'function'; } -/** Asserts that the provided value implements the {@link TransactionModifyingSigner} interface. */ +/** + * Asserts that the provided value implements the {@link TransactionModifyingSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { assertIsTransactionModifyingSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * assertIsTransactionModifyingSigner({ address, modifyAndSignTransactions: async () => {} }); // void + * assertIsTransactionModifyingSigner({ address }); // Throws an error. + * ``` + * + * @see {@link isTransactionModifyingSigner} + */ export function assertIsTransactionModifyingSigner(value: { [key: string]: unknown; address: Address; diff --git a/packages/signers/src/transaction-partial-signer.ts b/packages/signers/src/transaction-partial-signer.ts index 678dad34c..f2c418e82 100644 --- a/packages/signers/src/transaction-partial-signer.ts +++ b/packages/signers/src/transaction-partial-signer.ts @@ -4,9 +4,48 @@ import { Transaction } from '@solana/transactions'; import { BaseTransactionSignerConfig, SignatureDictionary } from './types'; +/** + * The configuration to optionally provide when calling the + * {@link TransactionPartialSigner#signTransactions | signTransactions} method. + * + * @see {@link BaseTransactionSignerConfig} + */ export type TransactionPartialSignerConfig = BaseTransactionSignerConfig; -/** Defines a signer capable of signing transactions. */ +/** + * A signer interface that signs an array of {@link Transaction | Transactions} + * without modifying their content. It defines a + * {@link TransactionPartialSigner#signTransactions | signTransactions} + * function that returns a {@link SignatureDictionary} for each provided transaction. + * + * Such signature dictionaries are expected to be merged with the existing ones if any. + * + * @typeParam TAddress - Supply a string literal to define a signer having a particular address. + * + * @example + * ```ts + * const signer: TransactionPartialSigner<'1234..5678'> = { + * address: address('1234..5678'), + * signTransactions: async ( + * transactions: Transaction[] + * ): Promise => { + * // My custom signing logic. + * }, + * }; + * ``` + * + * @remarks + * Here are the main characteristics of this signer interface: + * + * - **Parallel**. It returns a signature directory for each provided + * transaction without modifying them, making it possible for multiple + * partial signers to sign the same transaction in parallel. + * - **Flexible order**. The order in which we use these signers for + * a given transaction doesn’t matter. + * + * @see {@link isTransactionPartialSigner} + * @see {@link assertIsTransactionPartialSigner} + */ export type TransactionPartialSigner = Readonly<{ address: Address; signTransactions( @@ -15,7 +54,23 @@ export type TransactionPartialSigner = Readonl ): Promise; }>; -/** Checks whether the provided value implements the {@link TransactionPartialSigner} interface. */ +/** + * Checks whether the provided value implements the {@link TransactionPartialSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { isTransactionPartialSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * isTransactionPartialSigner({ address, signTransactions: async () => {} }); // true + * isTransactionPartialSigner({ address }); // false + * ``` + * + * @see {@link assertIsTransactionPartialSigner} + */ export function isTransactionPartialSigner(value: { [key: string]: unknown; address: Address; @@ -23,7 +78,23 @@ export function isTransactionPartialSigner(value: { return 'signTransactions' in value && typeof value.signTransactions === 'function'; } -/** Asserts that the provided value implements the {@link TransactionPartialSigner} interface. */ +/** + * Asserts that the provided value implements the {@link TransactionPartialSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { assertIsTransactionPartialSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * assertIsTransactionPartialSigner({ address, signTransactions: async () => {} }); // void + * assertIsTransactionPartialSigner({ address }); // Throws an error. + * ``` + * + * @see {@link isTransactionPartialSigner} + */ export function assertIsTransactionPartialSigner(value: { [key: string]: unknown; address: Address; diff --git a/packages/signers/src/transaction-sending-signer.ts b/packages/signers/src/transaction-sending-signer.ts index 8c91970f5..32c41d015 100644 --- a/packages/signers/src/transaction-sending-signer.ts +++ b/packages/signers/src/transaction-sending-signer.ts @@ -5,9 +5,59 @@ import { Transaction } from '@solana/transactions'; import { BaseTransactionSignerConfig } from './types'; +/** + * The configuration to optionally provide when calling the + * {@link TransactionSendingSignerConfig#signAndSendTransactions | signAndSendTransactions} method. + * + * @see {@link BaseTransactionSignerConfig} + */ export type TransactionSendingSignerConfig = BaseTransactionSignerConfig; -/** Defines a signer capable of signing and sending transactions simultaneously. */ +/** + * A signer interface that signs one or multiple transactions + * before sending them immediately to the blockchain. + * + * It defines a {@link TransactionSendingSignerConfig#signAndSendTransactions | signAndSendTransactions} + * function that returns the transaction signature (i.e. its identifier) for each provided + * {@link CompilableTransaction}. + * + * This interface is required for PDA wallets and other types of wallets that don't provide an + * interface for signing transactions without sending them. + * + * Note that it is also possible for such signers to modify the provided transactions + * before signing and sending them. This enables use cases where the modified transactions + * cannot be shared with the app and thus must be sent directly. + * + * @typeParam TAddress - Supply a string literal to define a signer having a particular address. + * + * @example + * ```ts + * const myTransactionSendingSigner: TransactionSendingSigner<'1234..5678'> = { + * address: address('1234..5678'), + * signAndSendTransactions: async (transactions: Transaction[]): Promise => { + * // My custom signing logic. + * }, + * }; + * ``` + * + * @remarks + * Here are the main characteristics of this signer interface: + * + * - **Single signer**. Since this signer also sends the provided transactions, + * we can only use a single {@link TransactionSendingSigner} for a given set of transactions. + * - **Last signer**. Trivially, that signer must also be the last one used. + * - **Potential conflicts**. Since signers may decide to modify the given + * transactions before sending them, they may invalidate previous signatures. + * However, signers may decide not to modify a transaction based + * on the existence of signatures for that transaction. + * - **Potential confirmation**. Whilst this is not required by this interface, + * it is also worth noting that most wallets will also wait for the transaction + * to be confirmed (typically with a `confirmed` commitment) + * before notifying the app that they are done. + * + * @see {@link isTransactionSendingSigner} + * @see {@link assertIsTransactionSendingSigner} + */ export type TransactionSendingSigner = Readonly<{ address: Address; signAndSendTransactions( @@ -16,7 +66,23 @@ export type TransactionSendingSigner = Readonl ): Promise; }>; -/** Checks whether the provided value implements the {@link TransactionSendingSigner} interface. */ +/** + * Checks whether the provided value implements the {@link TransactionSendingSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { isTransactionSendingSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * isTransactionSendingSigner({ address, signAndSendTransactions: async () => {} }); // true + * isTransactionSendingSigner({ address }); // false + * ``` + * + * @see {@link assertIsTransactionSendingSigner} + */ export function isTransactionSendingSigner(value: { [key: string]: unknown; address: Address; @@ -24,7 +90,23 @@ export function isTransactionSendingSigner(value: { return 'signAndSendTransactions' in value && typeof value.signAndSendTransactions === 'function'; } -/** Asserts that the provided value implements the {@link TransactionSendingSigner} interface. */ +/** + * Asserts that the provided value implements the {@link TransactionSendingSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { assertIsTransactionSendingSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * assertIsTransactionSendingSigner({ address, signAndSendTransactions: async () => {} }); // void + * assertIsTransactionSendingSigner({ address }); // Throws an error. + * ``` + * + * @see {@link isTransactionSendingSigner} + */ export function assertIsTransactionSendingSigner(value: { [key: string]: unknown; address: Address; diff --git a/packages/signers/src/transaction-signer.ts b/packages/signers/src/transaction-signer.ts index 1f2a6eca1..b685929ac 100644 --- a/packages/signers/src/transaction-signer.ts +++ b/packages/signers/src/transaction-signer.ts @@ -5,13 +5,39 @@ import { isTransactionModifyingSigner, TransactionModifyingSigner } from './tran import { isTransactionPartialSigner, TransactionPartialSigner } from './transaction-partial-signer'; import { isTransactionSendingSigner, TransactionSendingSigner } from './transaction-sending-signer'; -/** Defines a signer capable of signing transactions. */ +/** + * Defines a signer capable of signing transactions. + * + * @see {@link TransactionModifyingSigner} For signers that can modify transactions before signing them. + * @see {@link TransactionPartialSigner} For signers that can be used in parallel. + * @see {@link TransactionSendingSigner} For signers that send transactions after signing them. + * @see {@link isTransactionSigner} + * @see {@link assertIsTransactionSigner} + */ export type TransactionSigner = | TransactionModifyingSigner | TransactionPartialSigner | TransactionSendingSigner; -/** Checks whether the provided value implements the {@link TransactionSigner} interface. */ +/** + * Checks whether the provided value implements the {@link TransactionSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { isTransactionSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * isTransactionSigner({ address, signTransaction: async () => {} }); // true + * isTransactionSigner({ address, modifyAndSignTransaction: async () => {} }); // true + * isTransactionSigner({ address, signAndSendTransaction: async () => {} }); // true + * isTransactionSigner({ address }); // false + * ``` + * + * @see {@link assertIsTransactionSigner} + */ export function isTransactionSigner(value: { [key: string]: unknown; address: Address; @@ -21,7 +47,25 @@ export function isTransactionSigner(value: { ); } -/** Asserts that the provided value implements the {@link TransactionSigner} interface. */ +/** + * Asserts that the provided value implements the {@link TransactionSigner} interface. + * + * @typeParam TAddress - The inferred type of the address provided. + * + * @example + * ```ts + * import { Address } from '@solana/addresses'; + * import { assertIsTransactionSigner } from '@solana/signers'; + * + * const address = '1234..5678' as Address<'1234..5678'>; + * assertIsTransactionSigner({ address, signTransactions: async () => {} }); // void + * assertIsTransactionSigner({ address, modifyAndSignTransactions: async () => {} }); // void + * assertIsTransactionSigner({ address, signAndSendTransactions: async () => {} }); // void + * assertIsTransactionSigner({ address }); // Throws an error. + * ``` + * + * @see {@link isTransactionSigner} + */ export function assertIsTransactionSigner(value: { [key: string]: unknown; address: Address; diff --git a/packages/signers/src/transaction-with-single-sending-signer.ts b/packages/signers/src/transaction-with-single-sending-signer.ts index 384cef390..7eb630045 100644 --- a/packages/signers/src/transaction-with-single-sending-signer.ts +++ b/packages/signers/src/transaction-with-single-sending-signer.ts @@ -10,12 +10,57 @@ import { isTransactionModifyingSigner } from './transaction-modifying-signer'; import { isTransactionPartialSigner } from './transaction-partial-signer'; import { isTransactionSendingSigner } from './transaction-sending-signer'; -/** Defines a transaction message with exactly one {@link TransactionSendingSigner}. */ +/** + * Defines a transaction message with exactly one {@link TransactionSendingSigner}. + * + * This type is used to narrow the type of transaction messages that have been + * checked to have exactly one sending signer. + * + * @example + * ```ts + * import { assertIsTransactionMessageWithSingleSendingSigner } from '@solana/signers'; + * + * assertIsTransactionMessageWithSingleSendingSigner(transactionMessage); + * transactionMessage satisfies ITransactionMessageWithSingleSendingSigner; + * ``` + * + * @see {@link isTransactionMessageWithSingleSendingSigner} + * @see {@link assertIsTransactionMessageWithSingleSendingSigner} + */ export type ITransactionMessageWithSingleSendingSigner = ITransactionMessageWithSigners & { readonly __transactionWithSingleSendingSigner: unique symbol; }; -/** Checks whether the provided transaction has exactly one {@link TransactionSendingSigner}. */ +/** + * Checks whether the provided transaction has exactly one {@link TransactionSendingSigner}. + * + * This can be useful when using {@link signAndSendTransactionMessageWithSigners} to provide + * a fallback strategy in case the transaction message cannot be send using this function. + * + * @typeParam TTransactionMessage - The inferred type of the transaction message provided. + * + * @example + * ```ts + * import { + * isTransactionMessageWithSingleSendingSigner, + * signAndSendTransactionMessageWithSigners, + * signTransactionMessageWithSigners, + * } from '@solana/signers'; + * import { getBase64EncodedWireTransaction } from '@solana/transactions'; + * + * let transactionSignature: SignatureBytes; + * if (isTransactionMessageWithSingleSendingSigner(transactionMessage)) { + * transactionSignature = await signAndSendTransactionMessageWithSigners(transactionMessage); + * } else { + * const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); + * const encodedTransaction = getBase64EncodedWireTransaction(signedTransaction); + * transactionSignature = await rpc.sendTransaction(encodedTransaction).send(); + * } + * ``` + * + * @see {@link signAndSendTransactionMessageWithSigners} + * @see {@link assertIsTransactionMessageWithSingleSendingSigner} + */ export function isTransactionMessageWithSingleSendingSigner( transaction: TTransactionMessage, ): transaction is ITransactionMessageWithSingleSendingSigner & TTransactionMessage { @@ -27,7 +72,28 @@ export function isTransactionMessageWithSingleSendingSigner( diff --git a/packages/signers/src/types.ts b/packages/signers/src/types.ts index 1a6aac6f6..3cb3e9263 100644 --- a/packages/signers/src/types.ts +++ b/packages/signers/src/types.ts @@ -4,13 +4,34 @@ import { Slot } from '@solana/rpc-types'; export type SignatureDictionary = Readonly>; +/** + * The base configuration object for all signers — including transaction and message signers. + */ export type BaseSignerConfig = Readonly<{ + /** + * An optional `AbortSignal` that can be used to cancel the signing process. + * + * @example + * ```ts + * import { generateKeyPairSigner } from '@solana/signers'; + * + * const abortController = new AbortController(); + * const signer = await generateKeyPairSigner(); + * signer.signMessages([message], { abortSignal: abortController.signal }); + * abortController.abort(); + * ``` + */ abortSignal?: AbortSignal; }>; +/** + * The base configuration object for transaction signers only. + */ export interface BaseTransactionSignerConfig extends BaseSignerConfig { - // Signers that simulate transactions (eg. wallets) might be interested in knowing which slot - // was current when the transaction was prepared. They can use this information to ensure that - // they don't run the simulation at too early a slot. + /** + * Signers that simulate transactions (eg. wallets) might be interested in knowing which slot + * was current when the transaction was prepared. They can use this information to ensure that + * they don't run the simulation at too early a slot. + */ minContextSlot?: Slot; } diff --git a/packages/signers/typedoc.json b/packages/signers/typedoc.json index d99f37ef3..3a90a65cc 100644 --- a/packages/signers/typedoc.json +++ b/packages/signers/typedoc.json @@ -1,5 +1,6 @@ { "$schema": "https://typedoc.org/schema.json", "extends": ["../typedoc.base.json"], - "entryPoints": ["src/index.ts"] + "entryPoints": ["src/index.ts"], + "readme": "none" }