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

228 lines
6.7 KiB
TypeScript
Raw Normal View History

2021-05-08 14:52:26 -07:00
import camelCase from "camelcase";
import EventEmitter from "eventemitter3";
import * as bs58 from "bs58";
import {
Keypair,
2021-05-08 14:52:26 -07:00
PublicKey,
SystemProgram,
TransactionInstruction,
Commitment,
} from "@solana/web3.js";
import Provider from "../../provider";
import { Idl } from "../../idl";
import Coder, {
ACCOUNT_DISCRIMINATOR_SIZE,
accountDiscriminator,
accountSize,
} from "../../coder";
import { Subscription } from "../common";
/**
* Accounts is a dynamically generated object to fetch any given account
* of a program.
*/
2021-05-10 13:12:20 -07:00
export interface AccountNamespace {
2021-05-08 14:52:26 -07:00
[key: string]: AccountFn;
}
/**
* Account is a function returning a deserialized account, given an address.
*/
export type AccountFn<T = any> = AccountProps & ((address: PublicKey) => T);
/**
* Non function properties on the acccount namespace.
*/
type AccountProps = {
size: number;
all: (filter?: Buffer) => Promise<ProgramAccount<any>[]>;
subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter;
unsubscribe: (address: PublicKey) => void;
createInstruction: (keypair: Keypair) => Promise<TransactionInstruction>;
2021-05-08 14:52:26 -07:00
associated: (...args: PublicKey[]) => Promise<any>;
associatedAddress: (...args: PublicKey[]) => Promise<PublicKey>;
};
/**
2021-05-10 13:12:20 -07:00
* @hidden
*
2021-05-08 14:52:26 -07:00
* 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();
2021-05-10 13:12:20 -07:00
export default class AccountFactory {
2021-05-08 14:52:26 -07:00
// Returns the generated accounts namespace.
public static build(
idl: Idl,
coder: Coder,
programId: PublicKey,
provider: Provider
2021-05-10 13:12:20 -07:00
): AccountNamespace {
const accountFns: AccountNamespace = {};
2021-05-08 14:52:26 -07:00
idl.accounts.forEach((idlAccount) => {
const name = camelCase(idlAccount.name);
// Fetches the decoded account from the network.
const accountsNamespace = async (address: PublicKey): Promise<any> => {
const accountInfo = await provider.connection.getAccountInfo(address);
if (accountInfo === null) {
throw new Error(`Account does not exist ${address.toString()}`);
}
// Assert the account discriminator is correct.
const discriminator = await accountDiscriminator(idlAccount.name);
if (discriminator.compare(accountInfo.data.slice(0, 8))) {
throw new Error("Invalid account discriminator");
}
return coder.accounts.decode(idlAccount.name, accountInfo.data);
};
// Returns the size of the account.
// @ts-ignore
accountsNamespace["size"] =
ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount);
// Returns an instruction for creating this account.
// @ts-ignore
accountsNamespace["createInstruction"] = async (
keypair: Keypair,
2021-05-08 14:52:26 -07:00
sizeOverride?: number
): Promise<TransactionInstruction> => {
// @ts-ignore
const size = accountsNamespace["size"];
return SystemProgram.createAccount({
fromPubkey: provider.wallet.publicKey,
newAccountPubkey: keypair.publicKey,
2021-05-08 14:52:26 -07:00
space: sizeOverride ?? size,
lamports: await provider.connection.getMinimumBalanceForRentExemption(
sizeOverride ?? size
),
programId,
});
};
// Subscribes to all changes to this account.
// @ts-ignore
accountsNamespace["subscribe"] = (
address: PublicKey,
commitment?: Commitment
): EventEmitter => {
if (subscriptions.get(address.toString())) {
return subscriptions.get(address.toString()).ee;
}
const ee = new EventEmitter();
const listener = provider.connection.onAccountChange(
address,
(acc) => {
const account = coder.accounts.decode(idlAccount.name, acc.data);
ee.emit("change", account);
},
commitment
);
subscriptions.set(address.toString(), {
ee,
listener,
});
return ee;
};
// Unsubscribes to account changes.
// @ts-ignore
accountsNamespace["unsubscribe"] = (address: PublicKey) => {
let sub = subscriptions.get(address.toString());
if (!sub) {
console.warn("Address is not subscribed");
return;
}
if (subscriptions) {
provider.connection
.removeAccountChangeListener(sub.listener)
.then(() => {
subscriptions.delete(address.toString());
})
.catch(console.error);
}
};
// Returns all instances of this account type for the program.
// @ts-ignore
accountsNamespace["all"] = async (
filter?: Buffer
): Promise<ProgramAccount<any>[]> => {
let bytes = await accountDiscriminator(idlAccount.name);
if (filter !== undefined) {
bytes = Buffer.concat([bytes, filter]);
}
// @ts-ignore
let resp = await provider.connection._rpcRequest("getProgramAccounts", [
programId.toBase58(),
{
commitment: provider.connection.commitment,
filters: [
{
memcmp: {
offset: 0,
bytes: bs58.encode(bytes),
},
},
],
},
]);
if (resp.error) {
console.error(resp);
throw new Error("Failed to get accounts");
}
return (
resp.result
// @ts-ignore
.map(({ pubkey, account: { data } }) => {
data = bs58.decode(data);
return {
publicKey: new PublicKey(pubkey),
account: coder.accounts.decode(idlAccount.name, data),
};
})
);
};
// Function returning the associated address. Args are keys to associate.
// Order matters.
accountsNamespace["associatedAddress"] = async (
...args: PublicKey[]
): Promise<PublicKey> => {
let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
args.forEach((arg) => {
seeds.push(arg.toBuffer());
});
const [assoc] = await PublicKey.findProgramAddress(seeds, programId);
return assoc;
};
// Function returning the associated account. Args are keys to associate.
// Order matters.
accountsNamespace["associated"] = async (
...args: PublicKey[]
): Promise<any> => {
const addr = await accountsNamespace["associatedAddress"](...args);
return await accountsNamespace(addr);
};
accountFns[name] = accountsNamespace;
});
return accountFns;
}
}