Skip to content

Commit

Permalink
Document @solana/accounts with TypeDoc (#51)
Browse files Browse the repository at this point in the history
This PR moves content from the `README` into the code.

Preview here: https://illustrious-gumption-b4d2fe.netlify.app/modules/_solana_accounts.html

Addresses #50
  • Loading branch information
steveluscher authored Jan 14, 2025
1 parent 9029dc1 commit e68945b
Show file tree
Hide file tree
Showing 8 changed files with 433 additions and 24 deletions.
7 changes: 4 additions & 3 deletions packages/accounts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const BaseAccount: BaseAccount = {
executable: false,
lamports: lamports(1_000_000_000n),
programAddress: address('1111..1111'),
space: 42n,
};
```

Expand All @@ -71,6 +72,7 @@ const myEncodedAccount: Account<Uint8Array, '1234..5678'> = {
executable: false,
lamports: lamports(1_000_000_000n),
programAddress: address('1111..1111'),
space: 42n,
};

// Decoded.
Expand All @@ -81,6 +83,7 @@ const myDecodedAccount: Account<MyAccountData, '1234..5678'> = {
executable: false,
lamports: lamports(1_000_000_000n),
programAddress: address('1111..1111'),
space: 42n,
};
```

Expand All @@ -100,9 +103,7 @@ const myExistingAccount: MaybeAccount<MyAccountData, '1234..5678'> = {
exists: true,
address: address('1234..5678'),
data: { name: 'Alice', age: 30 },
executable: false,
lamports: lamports(1_000_000_000n),
programAddress: address('1111..1111'),
// ...
};

// Account does not exist.
Expand Down
83 changes: 79 additions & 4 deletions packages/accounts/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,97 @@ import type { Address } from '@solana/addresses';
import { ReadonlyUint8Array } from '@solana/codecs-core';
import type { Lamports } from '@solana/rpc-types';

/** The amount of bytes required to store the base account information without its data. */
/**
* The number of bytes required to store the {@link BaseAccount} information without its data.
*
* @example
* ```ts
* const myTotalAccountSize = myAccountDataSize + BASE_ACCOUNT_SIZE;
* ```
*/
export const BASE_ACCOUNT_SIZE = 128;

/** Describe the generic account details applicable to every account. */
/**
* Defines the attributes common to all Solana accounts. Namely, it contains everything stored
* on-chain except the account data itself.
*
* @interface
*
* @example
* ```ts
* const BaseAccount: BaseAccount = {
* executable: false,
* lamports: lamports(1_000_000_000n),
* programAddress: address('1111..1111'),
* space: 42n,
* };
* ```
*/
export type BaseAccount = {
readonly executable: boolean;
readonly lamports: Lamports;
readonly programAddress: Address;
readonly space: bigint;
};

/** Defines a Solana account with its generic details and parsed or encoded data. */
/**
* Contains all the information relevant to a Solana account. It includes the account's address and
* data, as well as the properties of {@link BaseAccount}.
*
* @interface
*
* @typeParam TAddress - Supply a string literal to define an account having a particular address.
* @typeParam TData - The nature of this account's data. It can be represented as either a
* `Uint8Array` &ndash; meaning the account is encoded &ndash; or a custom data type &ndash; meaning
* the account is decoded.
*
* @example
* ```ts
* // Encoded
* const myEncodedAccount: Account<Uint8Array, '1234..5678'> = {
* address: address('1234..5678'),
* data: new Uint8Array([1, 2, 3]),
* executable: false,
* lamports: lamports(1_000_000_000n),
* programAddress: address('1111..1111'),
* space: 42n,
* };
*
* // Decoded
* type MyAccountData = { name: string; age: number };
* const myDecodedAccount: Account<MyAccountData, '1234..5678'> = {
* address: address('1234..5678'),
* data: { name: 'Alice', age: 30 },
* executable: false,
* lamports: lamports(1_000_000_000n),
* programAddress: address('1111..1111'),
* space: 42n,
* };
* ```
*/
export type Account<TData extends Uint8Array | object, TAddress extends string = string> = BaseAccount & {
readonly address: Address<TAddress>;
readonly data: TData;
};

/** Defines a Solana account with its generic details and encoded data. */
/**
* Represents an encoded account and is equivalent to an {@link Account} with `Uint8Array` account
* data.
*
* @interface
*
* @typeParam TAddress - Supply a string literal to define an account having a particular address.
*
* @example
* ```ts
* {
* address: address('1234..5678'),
* data: new Uint8Array([1, 2, 3]),
* executable: false,
* lamports: lamports(1_000_000_000n),
* programAddress: address('1111..1111'),
* space: 42n,
* } satisfies EncodedAccount<'1234..5678'>;
* ```
*/
export type EncodedAccount<TAddress extends string = string> = Account<ReadonlyUint8Array, TAddress>;
77 changes: 74 additions & 3 deletions packages/accounts/src/decode-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,28 @@ import {
import type { Account, EncodedAccount } from './account';
import type { MaybeAccount, MaybeEncodedAccount } from './maybe-account';

/** Decodes the data of a given account using the provided decoder. */
/**
* Transforms an {@link EncodedAccount} into an {@link Account} (or a {@link MaybeEncodedAccount}
* into a {@link MaybeAccount}) by decoding the account data using the provided {@link Decoder}
* instance.
*
* @typeParam TAddress - Supply a string literal to define an account having a particular address.
* @typeParam TData - The type of this account's data.
*
* @example
* ```ts
* type MyAccountData = { name: string; age: number };
*
* const myAccount: EncodedAccount<'1234..5678'>;
* const myDecoder: Decoder<MyAccountData> = getStructDecoder([
* ['name', addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
* ['age', getU32Decoder()],
* ]);
*
* const myDecodedAccount = decodeAccount(myAccount, myDecoder);
* myDecodedAccount satisfies Account<MyAccountData, '1234..5678'>;
* ```
*/
export function decodeAccount<TData extends object, TAddress extends string = string>(
encodedAccount: EncodedAccount<TAddress>,
decoder: Decoder<TData>,
Expand Down Expand Up @@ -38,7 +59,39 @@ function accountExists<TData extends object>(account: Account<TData> | MaybeAcco
return !('exists' in account) || ('exists' in account && account.exists);
}

/** Asserts that an account has been decoded. */
/**
* Asserts that an account stores decoded data, ie. not a `Uint8Array`.
*
* Note that it does not check the shape of the data matches the decoded type, only that it is not a
* `Uint8Array`.
*
* @typeParam TAddress - Supply a string literal to define an account having a particular address.
* @typeParam TData - The type of this account's data.
*
* @example
* ```ts
* type MyAccountData = { name: string; age: number };
*
* const myAccount: Account<MyAccountData | Uint8Array, '1234..5678'>;
* assertAccountDecoded(myAccount);
*
* // now the account data can be used as MyAccountData
* account.data satisfies MyAccountData;
* ```
*
* This is particularly useful for narrowing the result of fetching a JSON parsed account.
*
* ```ts
* const account: MaybeAccount<MockData | Uint8Array> = await fetchJsonParsedAccount<MockData>(
* rpc,
* '1234..5678' as Address,
* );
*
* assertAccountDecoded(account);
* // now we have a MaybeAccount<MockData>
* account satisfies MaybeAccount<MockData>;
* ```
*/
export function assertAccountDecoded<TData extends object, TAddress extends string = string>(
account: Account<TData | Uint8Array, TAddress>,
): asserts account is Account<TData, TAddress>;
Expand All @@ -55,7 +108,25 @@ export function assertAccountDecoded<TData extends object, TAddress extends stri
}
}

/** Asserts that all accounts have been decoded. */
/**
* Asserts that all input accounts store decoded data, ie. not a `Uint8Array`.
*
* As with {@link assertAccountDecoded} it does not check the shape of the data matches the decoded
* type, only that it is not a `Uint8Array`.
*
* @example
* ```ts
* type MyAccountData = { name: string; age: number };
*
* const myAccounts: Account<MyAccountData | Uint8Array, Address>[];
* assertAccountsDecoded(myAccounts);
*
* // now the account data can be used as MyAccountData
* for (const a of account) {
* account.data satisfies MyAccountData;
* }
* ```
*/
export function assertAccountsDecoded<TData extends object, TAddress extends string = string>(
accounts: Account<ReadonlyUint8Array | TData, TAddress>[],
): asserts accounts is Account<TData, TAddress>[];
Expand Down
111 changes: 105 additions & 6 deletions packages/accounts/src/fetch-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,38 @@ import type { MaybeAccount, MaybeEncodedAccount } from './maybe-account';
import { parseBase64RpcAccount, parseJsonRpcAccount } from './parse-account';
import type { GetAccountInfoApi, GetMultipleAccountsApi } from './rpc-api';

/** Optional configuration for fetching a singular account. */
/**
* Optional configuration for fetching a singular account.
*
* @interface
*/
export type FetchAccountConfig = {
abortSignal?: AbortSignal;
commitment?: Commitment;
minContextSlot?: Slot;
};

/** Fetch a base64-encoded account that may or may not exist using an RPC client. */
/**
* Fetches a {@link MaybeEncodedAccount} from the provided RPC client and address.
*
* It uses the {@link GetAccountInfoApi.getAccountInfo | getAccountInfo} RPC method under the hood
* with base64 encoding and an additional configuration object can be provided to customize the
* behavior of the RPC call.
*
* @typeParam TAddress - Supply a string literal to define an account having a particular address.
*
* @example
* ```ts
* const myAddress = address('1234..5678');
* const myAccount: MaybeEncodedAccount<'1234..5678'> = await fetchEncodedAccount(rpc, myAddress);
*
* // With custom configuration.
* const myAccount: MaybeEncodedAccount<'1234..5678'> = await fetchEncodedAccount(rpc, myAddress, {
* abortSignal: myAbortController.signal,
* commitment: 'confirmed',
* });
* ```
*/
export async function fetchEncodedAccount<TAddress extends string = string>(
rpc: Rpc<GetAccountInfoApi>,
address: Address<TAddress>,
Expand All @@ -24,7 +48,31 @@ export async function fetchEncodedAccount<TAddress extends string = string>(
return parseBase64RpcAccount(address, response.value);
}

/** Fetch a json-parsed account that may or may not exist using an RPC client. */
/**
* Fetches a {@link MaybeAccount} from the provided RPC client and address by using
* {@link GetAccountInfoApi.getAccountInfo | getAccountInfo} under the hood with the `jsonParsed`
* encoding.
*
* It may also return a {@link MaybeEncodedAccount} if the RPC client does not know how to parse the
* account at the requested address. In any case, the expected data type should be explicitly
* provided as the first type parameter.
*
* @typeParam TAddress - Supply a string literal to define an account having a particular address.
* @typeParam TData - The expected type of this account's data.
*
* @example
* ```ts
* type TokenData = { mint: Address; owner: Address };
* const myAccount = await fetchJsonParsedAccount<TokenData>(rpc, myAddress);
* myAccount satisfies MaybeAccount<TokenData> | MaybeEncodedAccount;
*
* // With custom configuration.
* const myAccount = await fetchJsonParsedAccount<TokenData>(rpc, myAddress, {
* abortSignal: myAbortController.signal,
* commitment: 'confirmed',
* });
* ```
*/
export async function fetchJsonParsedAccount<TData extends object, TAddress extends string = string>(
rpc: Rpc<GetAccountInfoApi>,
address: Address<TAddress>,
Expand All @@ -39,14 +87,43 @@ export async function fetchJsonParsedAccount<TData extends object, TAddress exte
: parseBase64RpcAccount<TAddress>(address, account as Parameters<typeof parseBase64RpcAccount>[1]);
}

/** Optional configuration for fetching multiple accounts. */
/**
* Optional configuration for fetching multiple accounts.
*
* @interface
*/
export type FetchAccountsConfig = {
abortSignal?: AbortSignal;
commitment?: Commitment;
minContextSlot?: Slot;
};

/** Fetch multiple base64-encoded accounts that may or may not exist using an RPC client. */
/**
* Fetches an array of {@link MaybeEncodedAccount | MaybeEncodedAccounts} from the provided RPC
* client and an array of addresses.
*
* It uses the {@link GetMultipleAccountsApi#getMultipleAccounts | getMultipleAccounts} RPC method
* under the hood with base64 encodings and an additional configuration object can be provided to
* customize the behavior of the RPC call.
*
* @typeParam TAddresses - Supply an array of string literals to define accounts having particular
* addresses.
*
* @example
* ```ts
* const myAddressA = address('1234..5678');
* const myAddressB = address('8765..4321');
* const [myAccountA, myAccountB] = await fetchEncodedAccounts(rpc, [myAddressA, myAddressB]);
* myAccountA satisfies MaybeEncodedAccount<'1234..5678'>;
* myAccountB satisfies MaybeEncodedAccount<'8765..4321'>;
*
* // With custom configuration.
* const [myAccountA, myAccountB] = await fetchEncodedAccounts(rpc, [myAddressA, myAddressB], {
* abortSignal: myAbortController.signal,
* commitment: 'confirmed',
* });
* ```
*/
export async function fetchEncodedAccounts<
TAddresses extends string[] = string[],
TWrappedAddresses extends { [P in keyof TAddresses]: Address<TAddresses[P]> } = {
Expand All @@ -62,7 +139,29 @@ export async function fetchEncodedAccounts<
};
}

/** Fetch multiple json-parsed accounts that may or may not exist using an RPC client. */
/**
* Fetches an array of {@link MaybeAccount | MaybeAccounts} from a provided RPC client and an array
* of addresses.
*
* It uses the {@link GetMultipleAccountsApi#getMultipleAccounts | getMultipleAccounts} RPC method
* under the hood with the `jsonParsed` encoding. It may also return a
* {@link MaybeEncodedAccount} instead of the expected {@link MaybeAccount} if the RPC client does
* not know how to parse some of the requested accounts. In any case, the array of expected data
* types should be explicitly provided as the first type parameter.
*
* @typeParam TAddresses - Supply an array of string literals to define accounts having particular
* addresses.
* @typeParam TData - The expected types of these accounts' data.
* @example
* ```ts
* type TokenData = { mint: Address; owner: Address };
* type MintData = { supply: bigint };
* const [myAccountA, myAccountB] = await fetchJsonParsedAccounts<[TokenData, MintData]>(rpc, [myAddressA, myAddressB]);
* myAccountA satisfies MaybeAccount<TokenData> | MaybeEncodedAccount;
* myAccountB satisfies MaybeAccount<MintData> | MaybeEncodedAccount;
* ```
*/
export async function fetchJsonParsedAccounts<
TData extends object[],
TAddresses extends string[] = string[],
Expand Down
Loading

0 comments on commit e68945b

Please sign in to comment.