diff --git a/packages/rpc-api/src/getTransaction.ts b/packages/rpc-api/src/getTransaction.ts index 41b8dc560..df7eae30b 100644 --- a/packages/rpc-api/src/getTransaction.ts +++ b/packages/rpc-api/src/getTransaction.ts @@ -24,25 +24,37 @@ type ReturnData = { }; type TransactionMetaBase = Readonly<{ - /** The number of compute units consumed by the transaction */ + /** Number of compute units consumed by the transaction */ computeUnitsConsumed?: bigint; - /** If the transaction failed, this property will contain the error */ + /** Error if transaction failed, `null` if transaction succeeded. */ err: TransactionError | null; /** The fee this transaction was charged, in {@link Lamports} */ fee: Lamports; - /** An array of string log messages or `null` if log message recording was disabled when this transaction was processed */ + /** + * String log messages or `null` if log message recording was not enabled during this + * transaction + */ logMessages: readonly string[] | null; - /** An array of account balances, in {@link Lamports}, after the transaction was processed */ + /** Account balances after the transaction was processed */ postBalances: readonly Lamports[]; - /** List of token balances from after the transaction was processed or omitted if token balance recording was disabled when this transaction was processed */ + /** + * List of token balances from after the transaction was processed or omitted if token balance + * recording was not yet enabled during this transaction + */ postTokenBalances?: readonly TokenBalance[]; - /** An array of account balances, in {@link Lamports}, before the transaction was processed */ + /** Account balances from before the transaction was processed */ preBalances: readonly Lamports[]; - /** List of token balances from before the transaction was processed or omitted if token balance recording was disabled when this transaction was processed */ + /** + * List of token balances from before the transaction was processed or omitted if token balance + * recording was not yet enabled during this transaction + */ preTokenBalances?: readonly TokenBalance[]; /** The most-recent return data generated by an instruction in the transaction */ returnData?: ReturnData; - /** Transaction-level rewards; currently only `"Rent"`, but other types may be added in the future */ + /** + * Transaction-level rewards; currently only `"Rent"`, but other types may be added in the + * future + */ rewards: readonly Reward[] | null; /** @deprecated */ status: TransactionStatus; @@ -51,32 +63,94 @@ type TransactionMetaBase = Readonly<{ type AddressTableLookup = Readonly<{ /** The address of the address lookup table account. */ accountKey: Address; - /** The list of indices used to load addresses of readonly accounts from the lookup table. */ + /** Indices of accounts in a lookup table to load as read-only. */ readableIndexes: readonly number[]; - /** The list of indices used to load addresses of writable accounts from the lookup table. */ + /** Indices of accounts in a lookup table to load as writable. */ writableIndexes: readonly number[]; }>; type TransactionBase = Readonly<{ message: { + /** + * For transactions whose lifetime is specified by a recent blockhash, this is that + * blockhash, and for transactions whose lifetime is specified by a durable nonce, this is + * the nonce value. + */ recentBlockhash: Blockhash; }; + /** + * An ordered list of signatures belonging to the accounts required to sign this transaction. + * + * Each signature is an Ed25519 signature of the transaction message using the private key + * associated with the account required to sign the transaction. + */ signatures: readonly Base58EncodedBytes[]; }>; -type TransactionInstruction = Readonly<{ - accounts: readonly number[]; +type InstructionWithStackHeight = Readonly<{ + /** + * A number indicating the height at which this instruction was called with respect to the + * bottom of the call stack denoted by `1` or `null`. + * + * For instance, an instruction explicitly declared in the transaction message will have a `1` + * or `null` height, the first instruction that it calls using a cross-program invocation (CPI) + * will have a height of 2, an instruction called by that instruction using a CPI will have a + * depth of 3, and so on. + */ + stackHeight: number; // FIXME(https://github.com/anza-xyz/agave/issues/5732) Should be `1` instead of `null` at base of stack +}>; + +type InstructionWithData = Readonly<{ + /** The input to the invoked program */ data: Base58EncodedBytes; - programIdIndex: number; - stackHeight?: number; }>; +type TransactionInstruction = InstructionWithData & + Partial & + Readonly<{ + /** + * An ordered list of indices that indicate which accounts in the transaction message's + * accounts list are loaded by this instruction. + */ + accounts: readonly number[]; + /** + * The index of the address in the transaction message's accounts list associated with the + * program to invoke. + */ + programIdIndex: number; + }>; + type TransactionJson = Readonly<{ message: { + /** An ordered list of addresses belonging to the accounts loaded by this transaction */ accountKeys: readonly Address[]; header: { + /** + * The number of read-only accounts in the static accounts list that must sign this + * transaction. + * + * Subtracting this number from `numRequiredSignatures` yields the index of the first + * read-only signer account in the static accounts list. + */ numReadonlySignedAccounts: number; + /** + * The number of accounts in the static accounts list that are neither writable nor + * signers. + * + * Adding this number to `numRequiredSignatures` yields the index of the first read-only + * non-signer account in the static accounts list. + */ numReadonlyUnsignedAccounts: number; + /** + * The number of accounts in the static accounts list that must sign this transaction. + * + * Subtracting `numReadonlySignedAccounts` from this number yields the number of + * writable signer accounts in the static accounts list. Writable signer accounts always + * begin at index zero in the static accounts list. + * + * This number itself is the index of the first non-signer account in the static + * accounts list. + */ numRequiredSignatures: number; }; instructions: readonly TransactionInstruction[]; @@ -84,32 +158,52 @@ type TransactionJson = Readonly<{ }> & TransactionBase; -type PartiallyDecodedTransactionInstruction = Readonly<{ - accounts: readonly Address[]; - data: Base58EncodedBytes; - programId: Address; - stackHeight?: number; -}>; +type PartiallyDecodedTransactionInstruction = InstructionWithData & + Partial & + Readonly<{ + /** An ordered list of addresses belonging to the accounts loaded by this instruction */ + accounts: readonly Address[]; + /** The address of the program to invoke */ + programId: Address; + }>; -type ParsedTransactionInstruction = Readonly<{ - parsed: { - info?: object; - type: string; - }; - program: string; - programId: Address; - stackHeight?: number; -}>; +type ParsedTransactionInstruction = Partial & + Readonly<{ + /** The output of the program's instruction parser */ + parsed: { + /** The instruction, as interpreted the program's instruction parser. */ + info?: object; + /** + * A label that indicates the type of the instruction, as determined by the program's + * instruction parser. + */ + type: string; + }; + /** The name of the program. */ + program: string; + /** The address of the program */ + programId: Address; + }>; type ParsedAccount = Readonly<{ + /** The address of the account */ pubkey: Address; + /** Whether this account is required to sign the transaction that it's a part of */ signer: boolean; + /** + * Indicates whether the account was statically declared in the transaction message or loaded + * from an address lookup table. + */ source: 'lookupTable' | 'transaction'; + /** Whether this account must be loaded with a write-lock */ writable: boolean; }>; type TransactionJsonParsed = Readonly<{ message: { + /** + * An ordered list of parsed accounts belonging to the accounts loaded by this transaction + */ accountKeys: readonly ParsedAccount[]; instructions: readonly (ParsedTransactionInstruction | PartiallyDecodedTransactionInstruction)[]; }; @@ -166,25 +260,29 @@ type GetTransactionApiResponseBase = Readonly<{ }>; type TransactionMetaLoadedAddresses = Readonly<{ - /** Transaction account addresses loaded from address lookup tables */ + /** Addresses loaded from lookup tables */ loadedAddresses: { - /** Ordered list of base-58 encoded addresses for writable accounts */ - readonly: readonly Address[]; /** Ordered list of base-58 encoded addresses for read-only accounts */ + readonly: readonly Address[]; + /** Ordered list of base-58 encoded addresses for writable accounts */ writable: readonly Address[]; }; }>; type InnerInstructions = Readonly<{ + /** The index of the instruction in the transaction */ index: number; + /** The instructions */ instructions: readonly TInstructionType[]; }>; type TransactionMetaInnerInstructionsNotParsed = Readonly<{ + /** A list of instructions called by programs via cross-program invocation (CPI) */ innerInstructions?: readonly InnerInstructions[] | null; }>; type TransactionMetaInnerInstructionsParsed = Readonly<{ + /** A list of instructions called by programs via cross-program invocation (CPI) */ innerInstructions?: | readonly InnerInstructions[] | null; @@ -192,6 +290,7 @@ type TransactionMetaInnerInstructionsParsed = Readonly<{ type TransactionAddressTableLookups = Readonly<{ message: Readonly<{ + /** A list of address tables and the accounts that this transaction loads from them */ addressTableLookups: readonly AddressTableLookup[]; }>; }>; diff --git a/packages/rpc-graphql/src/resolvers/block.ts b/packages/rpc-graphql/src/resolvers/block.ts index 3c7594568..2e2a93403 100644 --- a/packages/rpc-graphql/src/resolvers/block.ts +++ b/packages/rpc-graphql/src/resolvers/block.ts @@ -83,10 +83,15 @@ export const resolveBlock = (fieldName?: string) => { }) ] = data[0]; } else if (typeof data === 'object') { - const jsonParsedData = data; - jsonParsedData.message.instructions = mapJsonParsedInstructions( - jsonParsedData.message.instructions, - ) as unknown as (typeof jsonParsedData)['message']['instructions']; + const jsonParsedData: typeof data = { + ...data, + message: { + ...data.message, + instructions: mapJsonParsedInstructions( + data.message.instructions, + ) as unknown as (typeof jsonParsedData)['message']['instructions'], + }, + }; const loadedInnerInstructions = loadedTransaction.meta?.innerInstructions; if (loadedInnerInstructions) { diff --git a/packages/rpc-types/src/account-info.ts b/packages/rpc-types/src/account-info.ts index 4328c84f3..e7910c6ba 100644 --- a/packages/rpc-types/src/account-info.ts +++ b/packages/rpc-types/src/account-info.ts @@ -9,15 +9,15 @@ import type { import type { Lamports } from './lamports'; export type AccountInfoBase = Readonly<{ - /** indicates if the account contains a program (and is strictly read-only) */ + /** Indicates if the account contains a program (and is strictly read-only) */ executable: boolean; - /** number of lamports assigned to this account */ + /** Number of {@link Lamports} assigned to this account */ lamports: Lamports; - /** pubkey of the program this account has been assigned to */ + /** Address of the program this account has been assigned to */ owner: Address; - /** the epoch at which this account will next owe rent */ + /** The epoch at which this account will next owe rent */ rentEpoch: bigint; - /** the size of the account data in bytes (excluding the 128 bytes of header) */ + /** The size of the account data in bytes (excluding the 128 bytes of header) */ space: bigint; }>; diff --git a/packages/rpc-types/src/blockhash.ts b/packages/rpc-types/src/blockhash.ts index ffcc24bfc..8fb797eb8 100644 --- a/packages/rpc-types/src/blockhash.ts +++ b/packages/rpc-types/src/blockhash.ts @@ -31,6 +31,24 @@ function getMemoizedBase58Decoder(): Decoder { return memoizedBase58Decoder; } +/** + * A type guard that returns `true` if the input string conforms to the {@link Blockhash} type, and + * refines its type for use in your program. + * + * @example + * ```ts + * import { isBlockhash } from '@solana/rpc-types'; + * + * if (isBlockhash(blockhash)) { + * // At this point, `blockhash` has been refined to a + * // `Blockhash` that can be used with the RPC. + * const { value: isValid } = await rpc.isBlockhashValid(blockhash).send(); + * setBlockhashIsFresh(isValid); + * } else { + * setError(`${blockhash} is not a blockhash`); + * } + * ``` + */ export function isBlockhash(putativeBlockhash: string): putativeBlockhash is Blockhash { // Fast-path; see if the input string is of an acceptable length. if ( @@ -51,6 +69,31 @@ export function isBlockhash(putativeBlockhash: string): putativeBlockhash is Blo return true; } +/** + * From time to time you might acquire a string, that you expect to validate as a blockhash, from an + * untrusted network API or user input. Use this function to assert that such an arbitrary string is + * a base58-encoded blockhash. + * + * @example + * ```ts + * import { assertIsBlockhash } from '@solana/rpc-types'; + * + * // Imagine a function that determines whether a blockhash is fresh when a user submits a form. + * function handleSubmit() { + * // We know only that what the user typed conforms to the `string` type. + * const blockhash: string = blockhashInput.value; + * try { + * // If this type assertion function doesn't throw, then + * // Typescript will upcast `blockhash` to `Blockhash`. + * assertIsBlockhash(blockhash); + * // At this point, `blockhash` is a `Blockhash` that can be used with the RPC. + * const { value: isValid } = await rpc.isBlockhashValid(blockhash).send(); + * } catch (e) { + * // `blockhash` turned out not to be a base58-encoded blockhash + * } + * } + * ``` + */ export function assertIsBlockhash(putativeBlockhash: string): asserts putativeBlockhash is Blockhash { // Fast-path; see if the input string is of an acceptable length. if ( @@ -74,21 +117,85 @@ export function assertIsBlockhash(putativeBlockhash: string): asserts putativeBl } } +/** + * Combines _asserting_ that a string is a blockhash with _coercing_ it to the {@link Blockhash} + * type. It's most useful with untrusted input. + * + * @example + * ```ts + * import { blockhash } from '@solana/rpc-types'; + * + * const { value: isValid } = await rpc.isBlockhashValid(blockhash(blockhashFromUserInput)).send(); + * ``` + * + * > [!TIP] + * > When starting from a known-good blockhash as a string, it's more efficient to typecast it + * rather than to use the {@link blockhash} helper, because the helper unconditionally performs + * validation on its input. + * > + * > ```ts + * > import { Blockhash } from '@solana/rpc-types'; + * > + * > const blockhash = 'ABmPH5KDXX99u6woqFS5vfBGSNyKG42SzpvBMWWqAy48' as Blockhash; + * > ``` + */ export function blockhash(putativeBlockhash: string): Blockhash { assertIsBlockhash(putativeBlockhash); return putativeBlockhash; } +/** + * Returns an encoder that you can use to encode a base58-encoded blockhash to a byte array. + * + * @example + * ```ts + * import { getBlockhashEncoder } from '@solana/rpc-types'; + * + * const blockhash = 'ABmPH5KDXX99u6woqFS5vfBGSNyKG42SzpvBMWWqAy48' as Blockhash; + * const blockhashEncoder = getBlockhashEncoder(); + * const blockhashBytes = blockhashEncoder.encode(blockhash); + * // Uint8Array(32) [ + * // 136, 123, 44, 249, 43, 19, 60, 14, + * // 144, 16, 168, 241, 121, 111, 70, 232, + * // 186, 26, 140, 202, 213, 64, 231, 82, + * // 179, 66, 103, 237, 52, 117, 217, 93 + * // ] + * ``` + */ export function getBlockhashEncoder(): FixedSizeEncoder { return transformEncoder(fixEncoderSize(getMemoizedBase58Encoder(), 32), putativeBlockhash => blockhash(putativeBlockhash), ); } +/** + * Returns a decoder that you can use to convert an array of 32 bytes representing a blockhash to + * the base58-encoded representation of that blockhash. + * + * @example + * ```ts + * import { getBlockhashDecoder } from '@solana/rpc-types'; + * + * const blockhashBytes = new Uint8Array([ + * 136, 123, 44, 249, 43, 19, 60, 14, + * 144, 16, 168, 241, 121, 111, 70, 232, + * 186, 26, 140, 202, 213, 64, 231, 82, + * 179, 66, 103, 237, 52, 117, 217, 93 + * ]); + * const blockhashDecoder = getBlockhashDecoder(); + * const blockhash = blockhashDecoder.decode(blockhashBytes); // ABmPH5KDXX99u6woqFS5vfBGSNyKG42SzpvBMWWqAy48 + * ``` + */ export function getBlockhashDecoder(): FixedSizeDecoder { return fixDecoderSize(getMemoizedBase58Decoder(), 32) as FixedSizeDecoder; } +/** + * Returns a codec that you can use to encode from or decode to a base-58 encoded blockhash. + * + * @see {@link getBlockhashDecoder} + * @see {@link getBlockhashEncoder} + */ export function getBlockhashCodec(): FixedSizeCodec { return combineCodec(getBlockhashEncoder(), getBlockhashDecoder()); } diff --git a/packages/rpc-types/src/cluster-url.ts b/packages/rpc-types/src/cluster-url.ts index 5c8ed6bf0..f173f1cc1 100644 --- a/packages/rpc-types/src/cluster-url.ts +++ b/packages/rpc-types/src/cluster-url.ts @@ -3,12 +3,15 @@ export type DevnetUrl = string & { '~cluster': 'devnet' }; export type TestnetUrl = string & { '~cluster': 'testnet' }; export type ClusterUrl = DevnetUrl | MainnetUrl | TestnetUrl | string; +/** Given a URL casts it to a type that is only accepted where mainnet URLs are expected. */ export function mainnet(putativeString: string): MainnetUrl { return putativeString as MainnetUrl; } +/** Given a URL casts it to a type that is only accepted where devnet URLs are expected. */ export function devnet(putativeString: string): DevnetUrl { return putativeString as DevnetUrl; } +/** Given a URL casts it to a type that is only accepted where testnet URLs are expected. */ export function testnet(putativeString: string): TestnetUrl { return putativeString as TestnetUrl; } diff --git a/packages/rpc-types/src/commitment.ts b/packages/rpc-types/src/commitment.ts index 29023c754..a05708cc3 100644 --- a/packages/rpc-types/src/commitment.ts +++ b/packages/rpc-types/src/commitment.ts @@ -1,5 +1,11 @@ import { SOLANA_ERROR__INVARIANT_VIOLATION__SWITCH_MUST_BE_EXHAUSTIVE, SolanaError } from '@solana/errors'; +/** + * A union of all possible commitment statuses -- each a measure of the network confirmation and + * stake levels on a particular block. + * + * Read more about the statuses themselves, [here](https://docs.solana.com/cluster/commitments). + */ export type Commitment = 'confirmed' | 'finalized' | 'processed'; function getCommitmentScore(commitment: Commitment): number { diff --git a/packages/rpc-types/src/index.ts b/packages/rpc-types/src/index.ts index a915f7a8b..2c4f8314d 100644 --- a/packages/rpc-types/src/index.ts +++ b/packages/rpc-types/src/index.ts @@ -1,3 +1,11 @@ +/** + * This package defines types for values used in the + * [Solana JSON-RPC](https://docs.solana.com/api/http) and a series of helpers for working with + * them. It can be used standalone, but it is also exported as part of Kit + * [`@solana/kit`](https://github.com/anza-xyz/kit/tree/main/packages/kit). + * + * @packageDocumentation + */ export * from './account-filters'; export * from './account-info'; export * from './blockhash'; diff --git a/packages/rpc-types/src/lamports.ts b/packages/rpc-types/src/lamports.ts index db78fc252..9e76ce895 100644 --- a/packages/rpc-types/src/lamports.ts +++ b/packages/rpc-types/src/lamports.ts @@ -11,6 +11,11 @@ import { import { getU64Decoder, getU64Encoder, NumberCodec, NumberDecoder, NumberEncoder } from '@solana/codecs-numbers'; import { SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE, SolanaError } from '@solana/errors'; +/** + * Represents an integer value denominated in Lamports (ie. $1 \times 10^{-9}$ ◎). + * + * It is represented as a `bigint` in client code and an `u64` in server code. + */ export type Lamports = bigint & { readonly __brand: unique symbol }; // Largest possible value to be represented by a u64 @@ -29,16 +34,72 @@ function getMemoizedU64Decoder(): FixedSizeDecoder { return memoizedU64Decoder; } +/** + * This is a type guard that accepts a `bigint` as input. It will both return `true` if the integer + * conforms to the {@link Lamports} type and will refine the type for use in your program. + * + * @example + * ```ts + * import { isLamports } from '@solana/rpc-types'; + * + * if (isLamports(lamports)) { + * // At this point, `lamports` has been refined to a + * // `Lamports` that can be used anywhere Lamports are expected. + * await transfer(fromAddress, toAddress, lamports); + * } else { + * setError(`${lamports} is not a quantity of Lamports`); + * } + * ``` + */ export function isLamports(putativeLamports: bigint): putativeLamports is Lamports { return putativeLamports >= 0 && putativeLamports <= maxU64Value; } +/** + * Lamport values returned from the RPC API conform to the type {@link Lamports}. You can use a + * value of that type wherever a quantity of Lamports is expected. + * + * @example + * From time to time you might acquire a number that you expect to be a quantity of Lamports, from + * an untrusted network API or user input. To assert that such an arbitrary number is usable as a + * quantity of Lamports, use this function. + * + * ```ts + * import { assertIsLamports } from '@solana/rpc-types'; + * + * // Imagine a function that creates a transfer instruction when a user submits a form. + * function handleSubmit() { + * // We know only that what the user typed conforms to the `number` type. + * const lamports: number = parseInt(quantityInput.value, 10); + * try { + * // If this type assertion function doesn't throw, then + * // Typescript will upcast `lamports` to `Lamports`. + * assertIsLamports(lamports); + * // At this point, `lamports` is a `Lamports` that can be used anywhere Lamports are expected. + * await transfer(fromAddress, toAddress, lamports); + * } catch (e) { + * // `lamports` turned out not to validate as a quantity of Lamports. + * } + * } + * ``` + */ export function assertIsLamports(putativeLamports: bigint): asserts putativeLamports is Lamports { if (putativeLamports < 0 || putativeLamports > maxU64Value) { throw new SolanaError(SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE); } } +/** + * This helper combines _asserting_ that a number is a possible number of {@link Lamports} with + * _coercing_ it to the {@link Lamports} type. It's best used with untrusted input. + * + * @example + * ```ts + * import { lamports } from '@solana/rpc-types'; + * + * await transfer(address(fromAddress), address(toAddress), lamports(100000n)); + * ``` + */ export function lamports(putativeLamports: bigint): Lamports { assertIsLamports(putativeLamports); return putativeLamports; @@ -46,20 +107,61 @@ export function lamports(putativeLamports: bigint): Lamports { type ExtractAdditionalProps = Omit; +/** + * Returns an encoder that you can use to encode a 64-bit {@link Lamports} value to 8 bytes in + * little endian order. + */ export function getDefaultLamportsEncoder(): FixedSizeEncoder { return getLamportsEncoder(getMemoizedU64Encoder()); } +/** + * Returns an encoder that you can use to encode a {@link Lamports} value to a byte array. + * + * You must supply a number decoder that will determine how encode the numeric value. + * + * @example + * ```ts + * import { getLamportsEncoder } from '@solana/rpc-types'; + * import { getU16Encoder } from '@solana/codecs-numbers'; + * + * const lamports = lamports(256n); + * const lamportsEncoder = getLamportsEncoder(getU16Encoder()); + * const lamportsBytes = lamportsEncoder.encode(lamports); + * // Uint8Array(2) [ 0, 1 ] + * ``` + */ export function getLamportsEncoder( innerEncoder: TEncoder, ): Encoder & ExtractAdditionalProps { return innerEncoder; } +/** + * Returns a decoder that you can use to decode a byte array representing a 64-bit little endian + * number to a {@link Lamports} value. + */ export function getDefaultLamportsDecoder(): FixedSizeDecoder { return getLamportsDecoder(getMemoizedU64Decoder()); } +/** + * Returns a decoder that you can use to convert an array of bytes representing a number to a + * {@link Lamports} value. + * + * You must supply a number decoder that will determine how many bits to use to decode the numeric + * value. + * + * @example + * ```ts + * import { getLamportsDecoder } from '@solana/rpc-types'; + * import { getU16Decoder } from '@solana/codecs-numbers'; + * + * const lamportsBytes = new Uint8Array([ 0, 1 ]); + * const lamportsDecoder = getLamportsDecoder(getU16Decoder()); + * const lamports = lamportsDecoder.decode(lamportsBytes); // lamports(256n) + * ``` + */ export function getLamportsDecoder( innerDecoder: TDecoder, ): Decoder & ExtractAdditionalProps { @@ -68,10 +170,22 @@ export function getLamportsDecoder( ) as Decoder & ExtractAdditionalProps; } +/** + * Returns a codec that you can use to encode from or decode to a 64-bit {@link Lamports} value. + * + * @see {@link getDefaultLamportsDecoder} + * @see {@link getDefaultLamportsEncoder} + */ export function getDefaultLamportsCodec(): FixedSizeCodec { return combineCodec(getDefaultLamportsEncoder(), getDefaultLamportsDecoder()); } +/** + * Returns a codec that you can use to encode from or decode to {@link Lamports} value. + * + * @see {@link getLamportsDecoder} + * @see {@link getLamportsEncoder} + */ export function getLamportsCodec( innerCodec: TCodec, ): Codec & ExtractAdditionalProps { diff --git a/packages/rpc-types/src/stringified-bigint.ts b/packages/rpc-types/src/stringified-bigint.ts index bea0f3448..6ec056b2d 100644 --- a/packages/rpc-types/src/stringified-bigint.ts +++ b/packages/rpc-types/src/stringified-bigint.ts @@ -1,7 +1,27 @@ import { SOLANA_ERROR__MALFORMED_BIGINT_STRING, SolanaError } from '@solana/errors'; +/** + * This type represents a `bigint` which has been encoded as a string for transit over a transport + * that does not support `bigint` values natively. The JSON-RPC is such a transport. + */ export type StringifiedBigInt = string & { readonly __brand: unique symbol }; +/** + * A type guard that returns `true` if the input string parses as a `BigInt`, and refines its type + * for use in your program. + * + * @example + * ```ts + * import { isStringifiedBigInt } from '@solana/rpc-types'; + * + * if (isStringifiedBigInt(bigintString)) { + * // At this point, `bigintString` has been refined to a `StringifiedBigInt` + * bigintString satisfies StringifiedBigInt; // OK + * } else { + * setError(`${bigintString} does not represent a BigInt`); + * } + * ``` + */ export function isStringifiedBigInt(putativeBigInt: string): putativeBigInt is StringifiedBigInt { try { BigInt(putativeBigInt); @@ -11,6 +31,28 @@ export function isStringifiedBigInt(putativeBigInt: string): putativeBigInt is S } } +/** + * From time to time you might acquire a string, that you expect to parse as a `BigInt`, from an + * untrusted network API or user input. Use this function to assert that such an arbitrary string + * will in fact parse as a `BigInt`. + * + * @example + * ```ts + * import { assertIsStringifiedBigInt } from '@solana/rpc-types'; + * + * // Imagine having received a value that you presume represents the supply of some token. + * // At this point we know only that it conforms to the `string` type. + * try { + * // If this type assertion function doesn't throw, then + * // Typescript will upcast `supplyString` to `StringifiedBigInt`. + * assertIsStringifiedBigInt(supplyString); + * // At this point, `supplyString` is a `StringifiedBigInt`. + * supplyString satisfies StringifiedBigInt; + * } catch (e) { + * // `supplyString` turned out not to parse as a `BigInt` + * } + * ``` + */ export function assertIsStringifiedBigInt(putativeBigInt: string): asserts putativeBigInt is StringifiedBigInt { try { BigInt(putativeBigInt); @@ -21,6 +63,17 @@ export function assertIsStringifiedBigInt(putativeBigInt: string): asserts putat } } +/** + * This helper combines _asserting_ that a string will parse as a `BigInt` with _coercing_ it to the + * {@link StringifiedBigInt} type. It's best used with untrusted input. + * + * @example + * ```ts + * import { stringifiedBigInt } from '@solana/rpc-types'; + * + * const supplyString = stringifiedBigInt('1000000000'); + * ``` + */ export function stringifiedBigInt(putativeBigInt: string): StringifiedBigInt { assertIsStringifiedBigInt(putativeBigInt); return putativeBigInt; diff --git a/packages/rpc-types/src/stringified-number.ts b/packages/rpc-types/src/stringified-number.ts index 6a581ff79..b62d3f823 100644 --- a/packages/rpc-types/src/stringified-number.ts +++ b/packages/rpc-types/src/stringified-number.ts @@ -1,11 +1,54 @@ import { SOLANA_ERROR__MALFORMED_NUMBER_STRING, SolanaError } from '@solana/errors'; +/** + * This type represents a number which has been encoded as a string for transit over a transport + * where loss of precision when using the native number type is a concern. The JSON-RPC is such a + * transport. + */ export type StringifiedNumber = string & { readonly __brand: unique symbol }; +/** + * A type guard that returns `true` if the input string parses as a `Number`, and refines its type + * for use in your program. + * + * @example + * ```ts + * import { isStringifiedNumber } from '@solana/rpc-types'; + * + * if (isStringifiedNumber(numericString)) { + * // At this point, `numericString` has been refined to a `StringifiedNumber` + * numericString satisfies StringifiedNumber; // OK + * } else { + * setError(`${numericString} does not represent a number`); + * } + * ``` + */ export function isStringifiedNumber(putativeNumber: string): putativeNumber is StringifiedNumber { return !Number.isNaN(Number(putativeNumber)); } +/** + * From time to time you might acquire a string, that you expect to parse as a `Number`, from an + * untrusted network API or user input. Use this function to assert that such an arbitrary string + * will in fact parse as a `Number`. + * + * @example + * ```ts + * import { assertIsStringifiedNumber } from '@solana/rpc-types'; + * + * // Imagine having received a value that you presume represents some decimal number. + * // At this point we know only that it conforms to the `string` type. + * try { + * // If this type assertion function doesn't throw, then + * // Typescript will upcast `decimalNumberString` to `StringifiedNumber`. + * assertIsStringifiedNumber(decimalNumberString); + * // At this point, `decimalNumberString` is a `StringifiedNumber`. + * decimalNumberString satisfies StringifiedNumber; + * } catch (e) { + * // `decimalNumberString` turned out not to parse as a number. + * } + * ``` + */ export function assertIsStringifiedNumber(putativeNumber: string): asserts putativeNumber is StringifiedNumber { if (Number.isNaN(Number(putativeNumber))) { throw new SolanaError(SOLANA_ERROR__MALFORMED_NUMBER_STRING, { @@ -14,6 +57,17 @@ export function assertIsStringifiedNumber(putativeNumber: string): asserts putat } } +/** + * This helper combines _asserting_ that a string will parse as a `Number` with _coercing_ it to the + * {@link StringifiedNumber} type. It's best used with untrusted input. + * + * @example + * ```ts + * import { stringifiedNumber } from '@solana/rpc-types'; + * + * const decimalNumberString = stringifiedNumber('-42.1'); + * ``` + */ export function stringifiedNumber(putativeNumber: string): StringifiedNumber { assertIsStringifiedNumber(putativeNumber); return putativeNumber; diff --git a/packages/rpc-types/src/token-balance.ts b/packages/rpc-types/src/token-balance.ts index 9af32f1f7..cdcee990b 100644 --- a/packages/rpc-types/src/token-balance.ts +++ b/packages/rpc-types/src/token-balance.ts @@ -5,11 +5,11 @@ import type { TokenAmount } from './token-amount'; export type TokenBalance = Readonly<{ /** Index of the account in which the token balance is provided for. */ accountIndex: number; - /** Pubkey of the token's mint. */ + /** Address of the token's mint. */ mint: Address; - /** Pubkey of token balance's owner. */ + /** Address of token balance's owner. */ owner?: Address; - /** Pubkey of the Token program that owns the account. */ + /** Address of the Token program that owns the account. */ programId?: Address; uiTokenAmount: TokenAmount; }>; diff --git a/packages/rpc-types/src/transaction.ts b/packages/rpc-types/src/transaction.ts index ce0297a42..50575f5cc 100644 --- a/packages/rpc-types/src/transaction.ts +++ b/packages/rpc-types/src/transaction.ts @@ -10,81 +10,178 @@ import type { SignedLamports } from './typed-numbers'; type TransactionVersion = 'legacy' | 0; type AddressTableLookup = Readonly<{ - /** public key for an address lookup table account. */ + /** Address of the address lookup table account. */ accountKey: Address; - /** List of indices used to load addresses of readonly accounts from a lookup table. */ + /** Indices of accounts in a lookup table to load as read-only. */ readableIndexes: readonly number[]; - /** List of indices used to load addresses of writable accounts from a lookup table. */ + /** Indices of accounts in a lookup table to load as writable. */ writableIndexes: readonly number[]; }>; -type ParsedTransactionInstruction = Readonly<{ - parsed: { - info?: object; - type: string; - }; - program: string; - programId: Address; - stackHeight?: number; +type InstructionWithStackHeight = Readonly<{ + /** + * A number indicating the height at which this instruction was called with respect to the + * bottom of the call stack denoted by `1` or `null`. + * + * For instance, an instruction explicitly declared in the transaction message will have a `1` + * or `null` height, the first instruction that it calls using a cross-program invocation (CPI) + * will have a height of 2, an instruction called by that instruction using a CPI will have a + * depth of 3, and so on. + */ + stackHeight: number; // FIXME(https://github.com/anza-xyz/agave/issues/5732) Should be `1` instead of `null` at base of stack }>; -type PartiallyDecodedTransactionInstruction = Readonly<{ - accounts: readonly Address[]; +type InstructionWithData = Readonly<{ + /** The input to the invoked program */ data: Base58EncodedBytes; - programId: Address; - stackHeight?: number; }>; +type ParsedTransactionInstruction = Partial & + Readonly<{ + /** The output of the program's instruction parser */ + parsed: { + /** The instruction, as interpreted the program's instruction parser. */ + info?: object; + /** + * A label that indicates the type of the instruction, as determined by the program's + * instruction parser. + */ + type: string; + }; + /** The name of the program. */ + program: string; + /** The address of the program */ + programId: Address; + }>; + +type PartiallyDecodedTransactionInstruction = InstructionWithData & + Partial & + Readonly<{ + /** An ordered list of addresses belonging to the accounts loaded by this instruction */ + accounts: readonly Address[]; + /** The address of the program to invoke */ + programId: Address; + }>; + type ReturnData = { - /** the return data itself */ + /** A tuple whose first element is the bytes of the return data as a base64-encoded string. */ data: Base64EncodedDataResponse; - /** the program that generated the return data */ + /** The address of the program that generated the return data */ programId: Address; }; -type TransactionInstruction = Readonly<{ - accounts: readonly number[]; - data: Base58EncodedBytes; - programIdIndex: number; - stackHeight?: number; +type TransactionInstruction = InstructionWithData & + Partial & + Readonly<{ + /** + * An ordered list of indices that indicate which accounts in the transaction message's + * accounts list are loaded by this instruction. + */ + accounts: readonly number[]; + /** + * The index of the address in the transaction message's accounts list associated with the + * program to invoke. + */ + programIdIndex: number; + }>; + +type TransactionMessageBase = Readonly<{ + header: { + /** + * The number of read-only accounts in the static accounts list that must sign this + * transaction. + * + * Subtracting this number from `numRequiredSignatures` yields the index of the first + * read-only signer account in the static accounts list. + */ + numReadonlySignedAccounts: number; + /** + * The number of accounts in the static accounts list that are neither writable nor signers. + * + * Adding this number to `numRequiredSignatures` yields the index of the first read-only + * non-signer account in the static accounts list. + */ + numReadonlyUnsignedAccounts: number; + /** + * The number of accounts in the static accounts list that must sign this transaction. + * + * Subtracting `numReadonlySignedAccounts` from this number yields the number of writable + * signer accounts in the static accounts list. Writable signer accounts always begin at + * index zero in the static accounts list. + * + * This number itself is the index of the first non-signer account in the static accounts + * list. + */ + numRequiredSignatures: number; + }; + /** + * For transactions whose lifetime is specified by a recent blockhash, this is that blockhash, + * and for transactions whose lifetime is specified by a durable nonce, this is the nonce value. + */ + recentBlockhash: Blockhash; }>; -type TransactionParsedAccountLegacy = Readonly<{ +type TransactionParsedAccountBase = Readonly<{ + /** The address of the account */ pubkey: Address; + /** Whether this account is required to sign the transaction that it's a part of */ signer: boolean; - source: 'transaction'; + /** Whether this account must be loaded with a write-lock */ writable: boolean; }>; + +type TransactionParsedAccountLegacy = Readonly<{ + /** Indicates that the account was statically declared in the transaction message */ + source: 'transaction'; +}> & + TransactionParsedAccountBase; + type TransactionParsedAccountVersioned = Readonly<{ - pubkey: Address; - signer: boolean; + /** + * Indicates whether the account was statically declared in the transaction message or loaded + * from an address lookup table. + */ source: 'lookupTable' | 'transaction'; - writable: boolean; -}>; +}> & + TransactionParsedAccountBase; // Types for `accounts` transactionDetails -// Only a partial version of the `TransactionMetaBase` type for when `transactionDetails: accounts` is provided. +// Only a partial version of the `TransactionMetaBase` type for when +// `transactionDetails: 'accounts'` is provided. type TransactionForAccountsMetaBase = Readonly<{ - /** Error if transaction failed, null if transaction succeeded. */ + /** Error if transaction failed, `null` if transaction succeeded. */ err: TransactionError | null; - /** fee this transaction was charged */ + /** The fee this transaction was charged, in {@link Lamports} */ fee: Lamports; - /** array of account balances after the transaction was processed */ + /** Account balances after the transaction was processed */ postBalances: readonly Lamports[]; - /** List of token balances from after the transaction was processed or omitted if token balance recording was not yet enabled during this transaction */ + /** + * List of token balances from after the transaction was processed or omitted if token balance + * recording was not yet enabled during this transaction + */ postTokenBalances?: readonly TokenBalance[]; - /** array of account balances from before the transaction was processed */ + /** Account balances from before the transaction was processed */ preBalances: readonly Lamports[]; - /** List of token balances from before the transaction was processed or omitted if token balance recording was not yet enabled during this transaction */ - preTokenBalances?: readonly TokenBalance[]; /** - * Transaction status - * @deprecated + * List of token balances from before the transaction was processed or omitted if token balance + * recording was not yet enabled during this transaction */ + preTokenBalances?: readonly TokenBalance[]; + /** @deprecated */ status: TransactionStatus; }>; +type TransactionWithSignatures = Readonly<{ + /** + * An ordered list of signatures belonging to the accounts required to sign this transaction. + * + * Each signature is an Ed25519 signature of the transaction message using the private key + * associated with the account required to sign the transaction. + */ + signatures: readonly Base58EncodedBytes[]; +}>; + // Accounts export type TransactionForAccounts = @@ -96,9 +193,8 @@ export type TransactionForAccounts; + }> & + TransactionWithSignatures; }> : Readonly<{ /** Transaction partial meta */ @@ -107,9 +203,8 @@ export type TransactionForAccounts; + }> & + TransactionWithSignatures; /** The transaction version */ version: TransactionVersion; }>; @@ -117,47 +212,33 @@ export type TransactionForAccounts; +}> & + TransactionForAccountsMetaBase; export type TransactionForFullMetaInnerInstructionsUnparsed = Readonly<{ + /** A list of instructions called by programs via cross-program invocation (CPI) */ innerInstructions: readonly Readonly<{ /** The index of the instruction in the transaction */ index: number; - /** The instruction */ + /** The instructions */ instructions: readonly TransactionInstruction[]; }>[]; }>; export type TransactionForFullMetaInnerInstructionsParsed = Readonly<{ + /** A list of instructions called by programs via cross-program invocation (CPI) */ innerInstructions: readonly Readonly<{ /** The index of the instruction in the transaction */ index: number; - /** The instruction */ + /** The instructions */ instructions: readonly (ParsedTransactionInstruction | PartiallyDecodedTransactionInstruction)[]; }>[]; }>; @@ -168,13 +249,16 @@ export type TransactionForFullMetaInnerInstructionsParsed = Readonly<{ type TransactionForFullMetaLoadedAddresses = Readonly<{ /** Addresses loaded from lookup tables */ loadedAddresses: { + /** Ordered list of base-58 encoded addresses for read-only accounts */ readonly: readonly Address[]; + /** Ordered list of base-58 encoded addresses for writable accounts */ writable: readonly Address[]; }; }>; type TransactionForFullTransactionAddressTableLookups = Readonly<{ message: { + /** A list of address tables and the accounts that this transaction loads from them */ addressTableLookups?: readonly AddressTableLookup[] | null; }; }>; @@ -228,17 +312,12 @@ export type TransactionForFullBase64; + }> & + TransactionMessageBase; +}> & + TransactionWithSignatures; export type TransactionForFullJsonParsed = TMaxSupportedTransactionVersion extends void @@ -246,6 +325,7 @@ export type TransactionForFullJsonParsed; }; @@ -258,6 +338,7 @@ export type TransactionForFullJsonParsed; }; @@ -267,18 +348,14 @@ export type TransactionForFullJsonParsed; + }> & + TransactionMessageBase; +}> & + TransactionWithSignatures; export type TransactionForFullJson = TMaxSupportedTransactionVersion extends void diff --git a/packages/rpc-types/src/unix-timestamp.ts b/packages/rpc-types/src/unix-timestamp.ts index b49ebb411..ca0e5b80b 100644 --- a/packages/rpc-types/src/unix-timestamp.ts +++ b/packages/rpc-types/src/unix-timestamp.ts @@ -1,15 +1,63 @@ import { SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE, SolanaError } from '@solana/errors'; +/** + * This type represents a Unix timestamp in _seconds_. + * + * It is represented as a `bigint` in client code and an `i64` in server code. + */ export type UnixTimestamp = bigint & { readonly __brand: unique symbol }; // Largest possible value to be represented by an i64 const maxI64Value = 9223372036854775807n; // 2n ** 63n - 1n const minI64Value = -9223372036854775808n; // -(2n ** 63n) +/** + * This is a type guard that accepts a `bigint` as input. It will both return `true` if the integer + * conforms to the {@link UnixTimestamp} type and will refine the type for use in your program. + * + * @example + * ```ts + * import { isUnixTimestamp } from '@solana/rpc-types'; + * + * if (isUnixTimestamp(timestamp)) { + * // At this point, `timestamp` has been refined to a + * // `UnixTimestamp` that can be used anywhere timestamps are expected. + * timestamp satisfies UnixTimestamp; + * } else { + * setError(`${timestamp} is not a Unix timestamp`); + * } + * ``` + */ + export function isUnixTimestamp(putativeTimestamp: bigint): putativeTimestamp is UnixTimestamp { return putativeTimestamp >= minI64Value && putativeTimestamp <= maxI64Value; } +/** + * Timestamp values returned from the RPC API conform to the type {@link UnixTimestamp}. You can use + * a value of that type wherever a timestamp is expected. + * + * @example + * From time to time you might acquire a number that you expect to be a timestamp, from an untrusted + * network API or user input. To assert that such an arbitrary number is usable as a Unix timestamp, + * use this function. + * + * ```ts + * import { assertIsUnixTimestamp } from '@solana/rpc-types'; + * + * // Imagine having received a value that you presume represents a timestamp. + * // At this point we know only that it conforms to the `bigint` type. + * try { + * // If this type assertion function doesn't throw, then + * // Typescript will upcast `timestamp` to `UnixTimestamp`. + * assertIsUnixTimestamp(timestamp); + * // At this point, `timestamp` is a `UnixTimestamp`. + * timestamp satisfies UnixTimestamp; + * } catch (e) { + * // `timestamp` turned out not to be a valid Unix timestamp + * } + * ``` + */ export function assertIsUnixTimestamp(putativeTimestamp: bigint): asserts putativeTimestamp is UnixTimestamp { if (putativeTimestamp < minI64Value || putativeTimestamp > maxI64Value) { throw new SolanaError(SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE, { @@ -18,6 +66,17 @@ export function assertIsUnixTimestamp(putativeTimestamp: bigint): asserts putati } } +/** + * This helper combines _asserting_ that a `bigint` represents a Unix timestamp with _coercing_ it + * to the {@link UnixTimestamp} type. It's best used with untrusted input. + * + * @example + * ```ts + * import { unixTimestamp } from '@solana/rpc-types'; + * + * const timestamp = unixTimestamp(-42n); // Wednesday, December 31, 1969 3:59:18 PM GMT-08:00 + * ``` + */ export function unixTimestamp(putativeTimestamp: bigint): UnixTimestamp { assertIsUnixTimestamp(putativeTimestamp); return putativeTimestamp; diff --git a/packages/subscribable/src/async-iterable.ts b/packages/subscribable/src/async-iterable.ts index 92d3a9512..7d6e6c212 100644 --- a/packages/subscribable/src/async-iterable.ts +++ b/packages/subscribable/src/async-iterable.ts @@ -8,13 +8,29 @@ import { AbortController } from '@solana/event-target-impl'; import { DataPublisher } from './data-publisher'; type Config = Readonly<{ + /** + * Triggering this abort signal will cause all iterators spawned from this iterator to return + * once they have published all queued messages. + */ abortSignal: AbortSignal; + /** + * Messages from this channel of `dataPublisher` will be the ones yielded through the iterators. + * + * Messages only begin to be queued after the first time an iterator begins to poll. Channel + * messages published before that time will be dropped. + */ dataChannelName: string; // FIXME: It would be nice to be able to constrain the type of `dataPublisher` to one that // definitely supports the `dataChannelName` and `errorChannelName` channels, and // furthermore publishes `TData` on the `dataChannelName` channel. This is more difficult // than it should be: https://tsplay.dev/NlZelW dataPublisher: DataPublisher; + /** + * Messages from this channel of `dataPublisher` will be the ones thrown through the iterators. + * + * Any new iterators created after the first error is encountered will reject with that error + * when polled. + */ errorChannelName: string; }>; @@ -58,6 +74,48 @@ function createExplicitAbortToken() { const UNINITIALIZED = Symbol(); +/** + * Returns an `AsyncIterable` given a data publisher. + * + * The iterable will produce iterators that vend messages published to `dataChannelName` and will + * throw the first time a message is published to `errorChannelName`. Triggering the abort signal + * will cause all iterators spawned from this iterator to return once they have published all queued + * messages. + * + * Things to note: + * + * - If a message is published over a channel before the `AsyncIterator` attached to it has polled + * for the next result, the message will be queued in memory. + * - Messages only begin to be queued after the first time an iterator begins to poll. Channel + * messages published before that time will be dropped. + * - If there are messages in the queue and an error occurs, all queued messages will be vended to + * the iterator before the error is thrown. + * - If there are messages in the queue and the abort signal fires, all queued messages will be + * vended to the iterator after which it will return. + * - Any new iterators created after the first error is encountered will reject with that error when + * polled. + * + * @param config + * + * @example + * ```ts + * const iterable = createAsyncIterableFromDataPublisher({ + * abortSignal: AbortSignal.timeout(10_000), + * dataChannelName: 'message', + * dataPublisher, + * errorChannelName: 'error', + * }); + * try { + * for await (const message of iterable) { + * console.log('Got message', message); + * } + * } catch (e) { + * console.error('An error was published to the error channel', e); + * } finally { + * console.log("It's been 10 seconds; that's enough for now."); + * } + * ``` + */ export function createAsyncIterableFromDataPublisher({ abortSignal, dataChannelName, diff --git a/packages/subscribable/src/data-publisher.ts b/packages/subscribable/src/data-publisher.ts index 59367bf2f..9fd804658 100644 --- a/packages/subscribable/src/data-publisher.ts +++ b/packages/subscribable/src/data-publisher.ts @@ -2,7 +2,29 @@ import { TypedEventEmitter, TypedEventTarget } from './event-emitter'; type UnsubscribeFn = () => void; +/** + * Represents an object with an `on` function that you can call to subscribe to certain data over a + * named channel. + * + * @example + * ```ts + * let dataPublisher: DataPublisher<{ error: SolanaError }>; + * dataPublisher.on('data', handleData); // ERROR. `data` is not a known channel name. + * dataPublisher.on('error', e => { + * console.error(e); + * }); // OK. + * ``` + */ export interface DataPublisher = Record> { + /** + * Call this to subscribe to data over a named channel. + * + * @param channelName The name of the channel on which to subscribe for messages + * @param subscriber The function to call when a message becomes available + * @param options.signal An abort signal you can fire to unsubscribe + * + * @returns A function that you can call to unsubscribe + */ on( channelName: TChannelName, subscriber: (data: TDataByChannelName[TChannelName]) => void, @@ -10,6 +32,23 @@ export interface DataPublisher { + * if (JSON.parse(message.data).id === 42) { + * console.log('Got response 42'); + * unsubscribe(); + * } + * }); + * ``` + */ export function getDataPublisherFromEventEmitter>( eventEmitter: TypedEventEmitter | TypedEventTarget, ): DataPublisher<{ diff --git a/packages/subscribable/src/demultiplex.ts b/packages/subscribable/src/demultiplex.ts index 0b7e21497..8a0cfb7ae 100644 --- a/packages/subscribable/src/demultiplex.ts +++ b/packages/subscribable/src/demultiplex.ts @@ -2,6 +2,37 @@ import { EventTarget } from '@solana/event-target-impl'; import { DataPublisher, getDataPublisherFromEventEmitter } from './data-publisher'; +/** + * Given a channel that carries messages for multiple subscribers on a single channel name, this + * function returns a new {@link DataPublisher} that splits them into multiple channel names. + * + * @param messageTransformer A function that receives the message as the first argument, and returns + * a tuple of the derived channel name and the message. + * + * @example + * Imagine a channel that carries multiple notifications whose destination is contained within the + * message itself. + * + * ```ts + * const demuxedDataPublisher = demultiplexDataPublisher(channel, 'message', message => { + * const destinationChannelName = `notification-for:${message.subscriberId}`; + * return [destinationChannelName, message]; + * }); + * ``` + * + * Now you can subscribe to _only_ the messages you are interested in, without having to subscribe + * to the entire `'message'` channel and filter out the messages that are not for you. + * + * ```ts + * demuxedDataPublisher.on( + * 'notification-for:123', + * message => { + * console.log('Got a message for subscriber 123', message); + * }, + * { signal: AbortSignal.timeout(5_000) }, + * ); + * ``` + */ export function demultiplexDataPublisher< TDataPublisher extends DataPublisher, const TChannelName extends Parameters[0], diff --git a/packages/subscribable/src/event-emitter.ts b/packages/subscribable/src/event-emitter.ts index 77839a5ea..fdc94b37c 100644 --- a/packages/subscribable/src/event-emitter.ts +++ b/packages/subscribable/src/event-emitter.ts @@ -1,6 +1,19 @@ type EventMap = Record; type Listener = ((evt: TEvent) => void) | { handleEvent(object: TEvent): void }; +/** + * This type allows you to type `addEventListener` and `removeEventListener` so that the call + * signature of the listener matches the event type given. + * + * @example + * ```ts + * const emitter: TypedEventEmitter<{ message: MessageEvent }> = new WebSocket('wss://api.devnet.solana.com'); + * emitter.addEventListener('data', handleData); // ERROR. `data` is not a known event type. + * emitter.addEventListener('message', message => { + * console.log(message.origin); // OK. `message` is a `MessageEvent` so it has an `origin` property. + * }); + * ``` + */ export interface TypedEventEmitter { addEventListener( type: TEventType, @@ -14,9 +27,18 @@ export interface TypedEventEmitter { ): void; } +// Why not just extend the interface above, rather than to copy/paste it? +// See https://github.com/microsoft/TypeScript/issues/60008 /** - * Why not just extend the interface above, rather than to copy/paste it? - * See https://github.com/microsoft/TypeScript/issues/60008 + * This type is a superset of `TypedEventEmitter` that allows you to constrain calls to + * `dispatchEvent`. + * + * @example + * ```ts + * const target: TypedEventTarget<{ candyVended: CustomEvent<{ flavour: string }> }> = new EventTarget(); + * target.dispatchEvent(new CustomEvent('candyVended', { detail: { flavour: 'raspberry' } })); // OK. + * target.dispatchEvent(new CustomEvent('candyVended', { detail: { flavor: 'raspberry' } })); // ERROR. Misspelling in detail. + * ``` */ export interface TypedEventTarget { addEventListener( diff --git a/packages/subscribable/src/index.ts b/packages/subscribable/src/index.ts index 43b987822..7d8f0ea7f 100644 --- a/packages/subscribable/src/index.ts +++ b/packages/subscribable/src/index.ts @@ -1,3 +1,11 @@ +/** + * This package contains utilities for creating subscription-based event targets. These differ from + * the `EventTarget` interface in that the method you use to add a listener returns an unsubscribe + * function. It is primarily intended for internal use -- particularly for those building + * {@link RpcSubscriptionChannel | RpcSubscriptionChannels} and associated infrastructure. + * + * @packageDocumentation + */ export * from './async-iterable'; export * from './data-publisher'; export * from './demultiplex';