anchor/ts/packages/anchor/src/program/namespace/account.ts

431 lines
11 KiB
TypeScript

import camelCase from "camelcase";
import EventEmitter from "eventemitter3";
import {
Signer,
PublicKey,
SystemProgram,
TransactionInstruction,
Commitment,
GetProgramAccountsFilter,
AccountInfo,
RpcResponseAndContext,
Context,
} from "@solana/web3.js";
import Provider, { getProvider } from "../../provider.js";
import { Idl, IdlAccountDef } from "../../idl.js";
import { Coder, BorshCoder } from "../../coder/index.js";
import { Subscription, Address, translateAddress } from "../common.js";
import { AllAccountsMap, IdlAccounts } from "./types.js";
import * as pubkeyUtil from "../../utils/pubkey.js";
import * as rpcUtil from "../../utils/rpc.js";
export default class AccountFactory {
public static build<IDL extends Idl>(
idl: IDL,
coder: Coder,
programId: PublicKey,
provider?: Provider
): AccountNamespace<IDL> {
const accountFns = {} as AccountNamespace<IDL>;
idl.accounts?.forEach((idlAccount) => {
const name = camelCase(idlAccount.name);
accountFns[name] = new AccountClient<IDL>(
idl,
idlAccount,
programId,
provider,
coder
);
});
return accountFns;
}
}
type NullableIdlAccount<IDL extends Idl> = IDL["accounts"] extends undefined
? IdlAccountDef
: NonNullable<IDL["accounts"]>[number];
/**
* The namespace provides handles to an [[AccountClient]] object for each
* account in a program.
*
* ## Usage
*
* ```javascript
* account.<account-client>
* ```
*
* ## Example
*
* To fetch a `Counter` account from the above example,
*
* ```javascript
* const counter = await program.account.counter.fetch(address);
* ```
*
* For the full API, see the [[AccountClient]] reference.
*/
export type AccountNamespace<IDL extends Idl = Idl> = {
[N in keyof AllAccountsMap<IDL>]: AccountClient<IDL, N>;
};
export class AccountClient<
IDL extends Idl = Idl,
N extends keyof IdlAccounts<IDL> = keyof IdlAccounts<IDL>,
A extends NullableIdlAccount<IDL> = NullableIdlAccount<IDL>,
T = IdlAccounts<IDL>[N]
> {
/**
* Returns the number of bytes in this account.
*/
get size(): number {
return this._size;
}
private _size: number;
/**
* Returns the program ID owning all accounts.
*/
get programId(): PublicKey {
return this._programId;
}
private _programId: PublicKey;
/**
* Returns the client's wallet and network provider.
*/
get provider(): Provider {
return this._provider;
}
private _provider: Provider;
/**
* Returns the coder.
*/
get coder(): Coder {
return this._coder;
}
private _coder: Coder;
/**
* Returns the idl account.
*/
get idlAccount(): A {
return this._idlAccount;
}
private _idlAccount: A;
constructor(
idl: IDL,
idlAccount: A,
programId: PublicKey,
provider?: Provider,
coder?: Coder
) {
this._idlAccount = idlAccount;
this._programId = programId;
this._provider = provider ?? getProvider();
this._coder = coder ?? new BorshCoder(idl);
this._size = this._coder.accounts.size(idlAccount);
}
/**
* Returns a deserialized account, returning null if it doesn't exist.
*
* @param address The address of the account to fetch.
*/
async fetchNullable(
address: Address,
commitment?: Commitment
): Promise<T | null> {
const { data } = await this.fetchNullableAndContext(address, commitment);
return data;
}
/**
* Returns a deserialized account along with the associated rpc response context, returning null if it doesn't exist.
*
* @param address The address of the account to fetch.
*/
async fetchNullableAndContext(
address: Address,
commitment?: Commitment
): Promise<{ data: T | null; context: Context }> {
const accountInfo = await this.getAccountInfoAndContext(
address,
commitment
);
const { value, context } = accountInfo;
return {
data:
value && value.data.length !== 0
? this._coder.accounts.decode<T>(this._idlAccount.name, value.data)
: null,
context,
};
}
/**
* Returns a deserialized account.
*
* @param address The address of the account to fetch.
*/
async fetch(address: Address, commitment?: Commitment): Promise<T> {
const { data } = await this.fetchNullableAndContext(address, commitment);
if (data === null) {
throw new Error(
`Account does not exist or has no data ${address.toString()}`
);
}
return data;
}
/**
* Returns a deserialized account along with the associated rpc response context.
*
* @param address The address of the account to fetch.
*/
async fetchAndContext(
address: Address,
commitment?: Commitment
): Promise<{ data: T | null; context: Context }> {
const { data, context } = await this.fetchNullableAndContext(
address,
commitment
);
if (data === null) {
throw new Error(`Account does not exist ${address.toString()}`);
}
return { data, context };
}
/**
* Returns multiple deserialized accounts.
* Accounts not found or with wrong discriminator are returned as null.
*
* @param addresses The addresses of the accounts to fetch.
*/
async fetchMultiple(
addresses: Address[],
commitment?: Commitment
): Promise<(T | null)[]> {
const accounts = await this.fetchMultipleAndContext(addresses, commitment);
return accounts.map((account) => (account ? account.data : null));
}
/**
* Returns multiple deserialized accounts.
* Accounts not found or with wrong discriminator are returned as null.
*
* @param addresses The addresses of the accounts to fetch.
*/
async fetchMultipleAndContext(
addresses: Address[],
commitment?: Commitment
): Promise<({ data: T; context: Context } | null)[]> {
const accounts = await rpcUtil.getMultipleAccountsAndContext(
this._provider.connection,
addresses.map((address) => translateAddress(address)),
commitment
);
// Decode accounts where discriminator is correct, null otherwise
return accounts.map((result) => {
if (result == null) {
return null;
}
const { account, context } = result;
return {
data: this._coder.accounts.decode(this._idlAccount.name, account.data),
context,
};
});
}
/**
* Returns all instances of this account type for the program.
*
* @param filters User-provided filters to narrow the results from `connection.getProgramAccounts`.
*
* When filters are not defined this method returns all
* the account instances.
*
* When filters are of type `Buffer`, the filters are appended
* after the discriminator.
*
* When filters are of type `GetProgramAccountsFilter[]`,
* filters are appended after the discriminator filter.
*/
async all(
filters?: Buffer | GetProgramAccountsFilter[]
): Promise<ProgramAccount<T>[]> {
const filter: { offset?: number; bytes?: string; dataSize?: number } =
this.coder.accounts.memcmp(
this._idlAccount.name,
filters instanceof Buffer ? filters : undefined
);
const coderFilters: GetProgramAccountsFilter[] = [];
if (filter?.offset != undefined && filter?.bytes != undefined) {
coderFilters.push({
memcmp: { offset: filter.offset, bytes: filter.bytes },
});
}
if (filter?.dataSize != undefined) {
coderFilters.push({ dataSize: filter.dataSize });
}
let resp = await this._provider.connection.getProgramAccounts(
this._programId,
{
commitment: this._provider.connection.commitment,
filters: [...coderFilters, ...(Array.isArray(filters) ? filters : [])],
}
);
return resp.map(({ pubkey, account }) => {
return {
publicKey: pubkey,
account: this._coder.accounts.decode(
this._idlAccount.name,
account.data
),
};
});
}
/**
* Returns an `EventEmitter` emitting a "change" event whenever the account
* changes.
*/
subscribe(address: Address, commitment?: Commitment): EventEmitter {
const sub = subscriptions.get(address.toString());
if (sub) {
return sub.ee;
}
const ee = new EventEmitter();
address = translateAddress(address);
const listener = this._provider.connection.onAccountChange(
address,
(acc) => {
const account = this._coder.accounts.decode(
this._idlAccount.name,
acc.data
);
ee.emit("change", account);
},
commitment
);
subscriptions.set(address.toString(), {
ee,
listener,
});
return ee;
}
/**
* Unsubscribes from the account at the given address.
*/
async unsubscribe(address: Address) {
let sub = subscriptions.get(address.toString());
if (!sub) {
console.warn("Address is not subscribed");
return;
}
if (subscriptions) {
await this._provider.connection
.removeAccountChangeListener(sub.listener)
.then(() => {
subscriptions.delete(address.toString());
})
.catch(console.error);
}
}
/**
* Returns an instruction for creating this account.
*/
async createInstruction(
signer: Signer,
sizeOverride?: number
): Promise<TransactionInstruction> {
const size = this.size;
if (this._provider.publicKey === undefined) {
throw new Error(
"This function requires the Provider interface implementor to have a 'publicKey' field."
);
}
return SystemProgram.createAccount({
fromPubkey: this._provider.publicKey,
newAccountPubkey: signer.publicKey,
space: sizeOverride ?? size,
lamports:
await this._provider.connection.getMinimumBalanceForRentExemption(
sizeOverride ?? size
),
programId: this._programId,
});
}
/**
* @deprecated since version 14.0.
*
* Function returning the associated account. Args are keys to associate.
* Order matters.
*/
async associated(...args: Array<PublicKey | Buffer>): Promise<T> {
const addr = await this.associatedAddress(...args);
return await this.fetch(addr);
}
/**
* @deprecated since version 14.0.
*
* Function returning the associated address. Args are keys to associate.
* Order matters.
*/
async associatedAddress(
...args: Array<PublicKey | Buffer>
): Promise<PublicKey> {
return await pubkeyUtil.associated(this._programId, ...args);
}
async getAccountInfo(
address: Address,
commitment?: Commitment
): Promise<AccountInfo<Buffer> | null> {
return await this._provider.connection.getAccountInfo(
translateAddress(address),
commitment
);
}
async getAccountInfoAndContext(
address: Address,
commitment?: Commitment
): Promise<RpcResponseAndContext<AccountInfo<Buffer> | null>> {
return await this._provider.connection.getAccountInfoAndContext(
translateAddress(address),
commitment
);
}
}
/**
* @hidden
*
* Deserialized account owned by a program.
*/
export type ProgramAccount<T = any> = {
publicKey: PublicKey;
account: T;
};
// Tracks all subscriptions.
const subscriptions: Map<string, Subscription> = new Map();