Skip to content

Commit

Permalink
Document @solana/addresses with TypeDoc
Browse files Browse the repository at this point in the history
  • Loading branch information
steveluscher committed Jan 16, 2025
1 parent d1c787c commit c209b98
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 7 deletions.
115 changes: 115 additions & 0 deletions packages/addresses/src/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import {
SolanaError,
} from '@solana/errors';

/**
* Represents a string that validates as a Solana address. Functions that require well-formed
* addresses should specify their inputs in terms of this type.
*
* Whenever you need to validate an arbitrary string as a base58-encoded address, use the
* {@link address}, {@link assertIsAddress}, or {@link isAddress} functions in this package.
*/
export type Address<TAddress extends string = string> = TAddress & {
readonly __brand: unique symbol;
};
Expand All @@ -33,6 +40,23 @@ function getMemoizedBase58Decoder(): Decoder<string> {
return memoizedBase58Decoder;
}

/**
* A type guard that returns `true` if the input string conforms to the {@link Address} type, and
* refines its type for use in your program.
*
* ```ts
* import { isAddress } from '@solana/addresses';
*
* if (isAddress(ownerAddress)) {
* // At this point, `ownerAddress` has been refined to a
* // `Address` that can be used with the RPC.
* const { value: lamports } = await rpc.getBalance(ownerAddress).send();
* setBalanceLamports(lamports);
* } else {
* setError(`${ownerAddress} is not an address`);
* }
* ```
*/
export function isAddress(putativeAddress: string): putativeAddress is Address<typeof putativeAddress> {
// Fast-path; see if the input string is of an acceptable length.
if (
Expand All @@ -52,6 +76,31 @@ export function isAddress(putativeAddress: string): putativeAddress is Address<t
}
}

/**
* From time to time you might acquire a string, that you expect to validate as an address or public
* key, from an untrusted network API or user input. Use this function to assert that such an
* arbitrary string is a base58-encoded address.
*
* @example
* ```ts
* import { assertIsAddress } from '@solana/addresses';
*
* // Imagine a function that fetches an account's balance when a user submits a form.
* function handleSubmit() {
* // We know only that what the user typed conforms to the `string` type.
* const address: string = accountAddressInput.value;
* try {
* // If this type assertion function doesn't throw, then
* // Typescript will upcast `address` to `Address`.
* assertIsAddress(address);
* // At this point, `address` is an `Address` that can be used with the RPC.
* const balanceInLamports = await rpc.getBalance(address).send();
* } catch (e) {
* // `address` turned out not to be a base58-encoded address
* }
* }
* ```
*/
export function assertIsAddress(putativeAddress: string): asserts putativeAddress is Address<typeof putativeAddress> {
// Fast-path; see if the input string is of an acceptable length.
if (
Expand All @@ -75,21 +124,87 @@ export function assertIsAddress(putativeAddress: string): asserts putativeAddres
}
}

/**
* Combines _asserting_ that a string is an address with _coercing_ it to the {@link Address} type.
* It's most useful with untrusted input.
*
* @example
* ```ts
* import { address } from '@solana/addresses';
*
* await transfer(address(fromAddress), address(toAddress), lamports(100000n));
* ```
*
* > [!TIP]
* > When starting from a known-good address as a string, it's more efficient to typecast it rather
* than to use the {@link address} helper, because the helper unconditionally performs validation on
* its input.
* >
* > ```ts
* > import { Address } from '@solana/addresses';
* >
* > const MEMO_PROGRAM_ADDRESS =
* > 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' as Address<'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'>;
* > ```
*/
export function address<TAddress extends string = string>(putativeAddress: TAddress): Address<TAddress> {
assertIsAddress(putativeAddress);
return putativeAddress as Address<TAddress>;
}

/**
* Returns an encoder that you can use to encode a base58-encoded address to a byte array.
*
* @example
* ```ts
* import { getAddressEncoder } from '@solana/addresses';
*
* const address = 'B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka' as Address;
* const addressEncoder = getAddressEncoder();
* const addressBytes = addressEncoder.encode(address);
* // Uint8Array(32) [
* // 150, 183, 190, 48, 171, 8, 39, 156,
* // 122, 213, 172, 108, 193, 95, 26, 158,
* // 149, 243, 115, 254, 20, 200, 36, 30,
* // 248, 179, 178, 232, 220, 89, 53, 127
* // ]
* ```
*/
export function getAddressEncoder(): FixedSizeEncoder<Address, 32> {
return transformEncoder(fixEncoderSize(getMemoizedBase58Encoder(), 32), putativeAddress =>
address(putativeAddress),
);
}

/**
* Returns a decoder that you can use to convert an array of 32 bytes representing an address to the
* base58-encoded representation of that address.
*
* @example
* ```ts
* import { getAddressDecoder } from '@solana/addresses';
*
* const addressBytes = new Uint8Array([
* 150, 183, 190, 48, 171, 8, 39, 156,
* 122, 213, 172, 108, 193, 95, 26, 158,
* 149, 243, 115, 254, 20, 200, 36, 30,
* 248, 179, 178, 232, 220, 89, 53, 127
* ]);
* const addressDecoder = getAddressDecoder();
* const address = addressDecoder.decode(address); // B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka
* ```
*/
export function getAddressDecoder(): FixedSizeDecoder<Address, 32> {
return fixDecoderSize(getMemoizedBase58Decoder(), 32) as FixedSizeDecoder<Address, 32>;
}

/**
* Returns the intersection of the values returned by {@link getAddressDecoder} and
* {@link getAddressEncoder}.
*
* @see {@link getAddressDecoder}
* @see {@link getAddressEncoder}
*/
export function getAddressCodec(): FixedSizeCodec<Address, Address, 32> {
return combineCodec(getAddressEncoder(), getAddressDecoder());
}
Expand Down
7 changes: 7 additions & 0 deletions packages/addresses/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* This package contains utilities for generating account addresses. 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).
*
* @packageDocumentation
*/
export * from './address';
export * from './program-derived-address';
export * from './public-key';
62 changes: 56 additions & 6 deletions packages/addresses/src/program-derived-address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,31 @@ import { Address, assertIsAddress, getAddressCodec, isAddress } from './address'
import { compressedPointBytesAreOnCurve } from './curve';

/**
* An address derived from a program address and a set of seeds.
* It includes the bump seed used to derive the address and
* ensure the address is not on the Ed25519 curve.
* A tuple representing a program derived address (derived from the address of some program and a
* set of seeds) and the associated bump seed used to ensure that the address, as derived, does not
* fall on the Ed25519 curve.
*
* Whenever you need to validate an arbitrary tuple as one that represents a program derived
* address, use the {@link assertIsProgramDerivedAddress} or {@link isProgramDerivedAddress}
* functions in this package.
*/
export type ProgramDerivedAddress<TAddress extends string = string> = Readonly<
[Address<TAddress>, ProgramDerivedAddressBump]
>;

/**
* A number between 0 and 255, inclusive.
* Represents an integer in the range [0,255] used in the derivation of a program derived address to
* ensure that it does not fall on the Ed25519 curve.
*/
export type ProgramDerivedAddressBump = number & {
readonly __brand: unique symbol;
};

/**
* Returns true if the input value is a program derived address.
* A type guard that returns `true` if the input tuple conforms to the {@link ProgramDerivedAddress}
* type, and refines its type for use in your program.
*
* @see The {@link isAddress} function for an example of how to use a type guard.
*/
export function isProgramDerivedAddress<TAddress extends string = string>(
value: unknown,
Expand All @@ -49,7 +57,10 @@ export function isProgramDerivedAddress<TAddress extends string = string>(
}

/**
* Fails if the input value is not a program derived address.
* In the event that you receive an address/bump-seed tuple from some untrusted source, use this
* function to assert that it conforms to the {@link ProgramDerivedAddress}.
*
* @see The {@link assertIsAddress} function for an example of how to use an assertion function.
*/
export function assertIsProgramDerivedAddress<TAddress extends string = string>(
value: unknown,
Expand Down Expand Up @@ -121,6 +132,28 @@ async function createProgramDerivedAddress({ programAddress, seeds }: ProgramDer
return base58EncodedAddressCodec.decode(addressBytes);
}

/**
* Given a program's {@link Address} and up to 16 {@link Seed | Seeds}, this method will return the
* program derived address (PDA) associated with each.
*
* @example
* ```ts
* import { getAddressEncoder, getProgramDerivedAddress } from '@solana/addresses';
*
* const addressEncoder = getAddressEncoder();
* const { bumpSeed, pda } = await getProgramDerivedAddress({
* programAddress: 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' as Address,
* seeds: [
* // Owner
* addressEncoder.encode('9fYLFVoVqwH37C3dyPi6cpeobfbQ2jtLpN5HgAYDDdkm' as Address),
* // Token program
* addressEncoder.encode('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' as Address),
* // Mint
* addressEncoder.encode('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' as Address),
* ],
* });
* ```
*/
export async function getProgramDerivedAddress({
programAddress,
seeds,
Expand All @@ -144,6 +177,23 @@ export async function getProgramDerivedAddress({
throw new SolanaError(SOLANA_ERROR__ADDRESSES__FAILED_TO_FIND_VIABLE_PDA_BUMP_SEED);
}

/**
* Returns a base58-encoded address derived from some base address, some program address, and a seed
* string or byte array.
*
* @example
* ```ts
* import { createAddressWithSeed } from '@solana/addresses';
*
* const derivedAddress = await createAddressWithSeed({
* // The private key associated with this address will be able to sign for `derivedAddress`.
* baseAddress: 'B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka' as Address,
* // Only this program will be able to write data to this account.
* programAddress: '445erYq578p2aERrGW9mn9KiYe3fuG6uHdcJ2LPPShGw' as Address,
* seed: 'data-account',
* });
* ```
*/
export async function createAddressWithSeed({ baseAddress, programAddress, seed }: SeedInput): Promise<Address> {
const { encode, decode } = getAddressCodec();

Expand Down
10 changes: 10 additions & 0 deletions packages/addresses/src/public-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import { SOLANA_ERROR__ADDRESSES__INVALID_ED25519_PUBLIC_KEY, SolanaError } from

import { Address, getAddressDecoder } from './address';

/**
* Given a public {@link CryptoKey}, this method will return its associated {@link Address}.
*
* @example
* ```ts
* import { getAddressFromPublicKey } from '@solana/addresses';
*
* const address = await getAddressFromPublicKey(publicKey);
* ```
*/
export async function getAddressFromPublicKey(publicKey: CryptoKey): Promise<Address> {
assertKeyExporterIsAvailable();
if (publicKey.type !== 'public' || publicKey.algorithm.name !== 'Ed25519') {
Expand Down
3 changes: 2 additions & 1 deletion packages/addresses/typedoc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"$schema": "https://typedoc.org/schema.json",
"extends": ["../typedoc.base.json"],
"entryPoints": ["src/index.ts"]
"entryPoints": ["src/index.ts"],
"readme": "none"
}

0 comments on commit c209b98

Please sign in to comment.