Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document @solana/addresses with TypeDoc #62

Merged
merged 1 commit into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,24 @@ 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.
*
* @example
* ```ts
mcintyre94 marked this conversation as resolved.
Show resolved Hide resolved
* 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 +77,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 +125,86 @@ 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
mcintyre94 marked this conversation as resolved.
Show resolved Hide resolved
mcintyre94 marked this conversation as resolved.
Show resolved Hide resolved
* > 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 a codec that you can use to encode from or decode to a base-58 encoded address.
*
* @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} interface.
*
* @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"
}
Loading