2021-05-08 14:52:26 -07:00
|
|
|
import camelCase from "camelcase";
|
|
|
|
import EventEmitter from "eventemitter3";
|
|
|
|
import * as bs58 from "bs58";
|
|
|
|
import {
|
2021-05-20 02:26:32 -07:00
|
|
|
Signer,
|
2021-05-08 14:52:26 -07:00
|
|
|
PublicKey,
|
|
|
|
SystemProgram,
|
|
|
|
TransactionInstruction,
|
|
|
|
Commitment,
|
|
|
|
} from "@solana/web3.js";
|
|
|
|
import Provider from "../../provider";
|
2021-05-25 20:04:05 -07:00
|
|
|
import { Idl, IdlTypeDef } from "../../idl";
|
2021-05-08 14:52:26 -07:00
|
|
|
import Coder, {
|
|
|
|
ACCOUNT_DISCRIMINATOR_SIZE,
|
|
|
|
accountDiscriminator,
|
|
|
|
accountSize,
|
|
|
|
} from "../../coder";
|
2021-05-23 09:58:15 -07:00
|
|
|
import { Subscription, Address, translateAddress } from "../common";
|
2021-05-25 20:04:05 -07:00
|
|
|
import { getProvider } from "../../";
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-10 13:12:20 -07:00
|
|
|
export default class AccountFactory {
|
2021-05-08 14:52:26 -07:00
|
|
|
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);
|
2021-05-25 20:04:05 -07:00
|
|
|
accountFns[name] = new AccountClient(
|
|
|
|
idl,
|
|
|
|
idlAccount,
|
|
|
|
programId,
|
|
|
|
provider,
|
|
|
|
coder
|
|
|
|
);
|
|
|
|
});
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
return accountFns;
|
|
|
|
}
|
|
|
|
}
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
/**
|
|
|
|
* 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 interface AccountNamespace {
|
|
|
|
[key: string]: AccountClient;
|
|
|
|
}
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
export class AccountClient {
|
|
|
|
/**
|
|
|
|
* Returns the number of bytes in this account.
|
|
|
|
*/
|
|
|
|
get size(): number {
|
|
|
|
return this._size;
|
|
|
|
}
|
|
|
|
private _size: number;
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
/**
|
|
|
|
* Returns the program ID owning all accounts.
|
|
|
|
*/
|
|
|
|
get programId(): PublicKey {
|
|
|
|
return this._programId;
|
|
|
|
}
|
|
|
|
private _programId: PublicKey;
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
/**
|
|
|
|
* Returns the cleint's wallet and network provider.
|
|
|
|
*/
|
|
|
|
get provider(): Provider {
|
|
|
|
return this._provider;
|
|
|
|
}
|
|
|
|
private _provider: Provider;
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
/**
|
|
|
|
* Returns the coder.
|
|
|
|
*/
|
|
|
|
get coder(): Coder {
|
|
|
|
return this._coder;
|
|
|
|
}
|
|
|
|
private _coder: Coder;
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
private _idlAccount: IdlTypeDef;
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
constructor(
|
|
|
|
idl: Idl,
|
|
|
|
idlAccount: IdlTypeDef,
|
|
|
|
programId: PublicKey,
|
|
|
|
provider?: Provider,
|
|
|
|
coder?: Coder
|
|
|
|
) {
|
|
|
|
this._idlAccount = idlAccount;
|
|
|
|
this._programId = programId;
|
|
|
|
this._provider = provider ?? getProvider();
|
|
|
|
this._coder = coder ?? new Coder(idl);
|
|
|
|
this._size = ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a deserialized account.
|
|
|
|
*
|
|
|
|
* @param address The address of the account to fetch.
|
|
|
|
*/
|
|
|
|
async fetch(address: Address): Promise<Object> {
|
|
|
|
const accountInfo = await this._provider.connection.getAccountInfo(
|
|
|
|
translateAddress(address)
|
|
|
|
);
|
|
|
|
if (accountInfo === null) {
|
|
|
|
throw new Error(`Account does not exist ${address.toString()}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Assert the account discriminator is correct.
|
|
|
|
const discriminator = await accountDiscriminator(this._idlAccount.name);
|
|
|
|
if (discriminator.compare(accountInfo.data.slice(0, 8))) {
|
|
|
|
throw new Error("Invalid account discriminator");
|
|
|
|
}
|
|
|
|
|
|
|
|
return this._coder.accounts.decode(this._idlAccount.name, accountInfo.data);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns all instances of this account type for the program.
|
|
|
|
*/
|
|
|
|
async all(filter?: Buffer): Promise<ProgramAccount<any>[]> {
|
|
|
|
let bytes = await accountDiscriminator(this._idlAccount.name);
|
|
|
|
if (filter !== undefined) {
|
|
|
|
bytes = Buffer.concat([bytes, filter]);
|
|
|
|
}
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
let resp = await this._provider.connection.getProgramAccounts(
|
|
|
|
this._programId,
|
|
|
|
{
|
|
|
|
commitment: this._provider.connection.commitment,
|
|
|
|
filters: [
|
2021-05-08 14:52:26 -07:00
|
|
|
{
|
2021-05-25 20:04:05 -07:00
|
|
|
memcmp: {
|
|
|
|
offset: 0,
|
|
|
|
bytes: bs58.encode(bytes),
|
|
|
|
},
|
2021-05-08 14:52:26 -07:00
|
|
|
},
|
2021-05-25 20:04:05 -07:00
|
|
|
],
|
|
|
|
}
|
|
|
|
);
|
|
|
|
return resp.map(({ pubkey, account }) => {
|
|
|
|
return {
|
|
|
|
publicKey: pubkey,
|
|
|
|
account: this._coder.accounts.decode(
|
|
|
|
this._idlAccount.name,
|
|
|
|
account.data
|
|
|
|
),
|
2021-05-08 14:52:26 -07:00
|
|
|
};
|
2021-05-25 20:04:05 -07:00
|
|
|
});
|
|
|
|
}
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
/**
|
|
|
|
* Returns an `EventEmitter` emitting a "change" event whenever the account
|
|
|
|
* changes.
|
|
|
|
*/
|
|
|
|
subscribe(address: Address, commitment?: Commitment): EventEmitter {
|
|
|
|
if (subscriptions.get(address.toString())) {
|
|
|
|
return subscriptions.get(address.toString()).ee;
|
|
|
|
}
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
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
|
|
|
|
);
|
2021-05-08 14:52:26 -07:00
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
subscriptions.set(address.toString(), {
|
|
|
|
ee,
|
|
|
|
listener,
|
2021-05-08 14:52:26 -07:00
|
|
|
});
|
|
|
|
|
2021-05-25 20:04:05 -07:00
|
|
|
return ee;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unsubscribes from the account at the given address.
|
|
|
|
*/
|
|
|
|
unsubscribe(address: Address) {
|
|
|
|
let sub = subscriptions.get(address.toString());
|
|
|
|
if (!sub) {
|
|
|
|
console.warn("Address is not subscribed");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (subscriptions) {
|
|
|
|
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;
|
|
|
|
|
|
|
|
return SystemProgram.createAccount({
|
|
|
|
fromPubkey: this._provider.wallet.publicKey,
|
|
|
|
newAccountPubkey: signer.publicKey,
|
|
|
|
space: sizeOverride ?? size,
|
|
|
|
lamports: await this._provider.connection.getMinimumBalanceForRentExemption(
|
|
|
|
sizeOverride ?? size
|
|
|
|
),
|
|
|
|
programId: this._programId,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function returning the associated account. Args are keys to associate.
|
|
|
|
* Order matters.
|
|
|
|
*/
|
|
|
|
async associated(...args: PublicKey[]): Promise<any> {
|
|
|
|
const addr = await this.associatedAddress(...args);
|
|
|
|
return await this.fetch(addr);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function returning the associated address. Args are keys to associate.
|
|
|
|
* Order matters.
|
|
|
|
*/
|
|
|
|
async associatedAddress(...args: PublicKey[]): Promise<PublicKey> {
|
|
|
|
let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
|
|
|
|
args.forEach((arg) => {
|
|
|
|
seeds.push(translateAddress(arg).toBuffer());
|
|
|
|
});
|
|
|
|
const [assoc] = await PublicKey.findProgramAddress(seeds, this._programId);
|
|
|
|
return assoc;
|
2021-05-08 14:52:26 -07:00
|
|
|
}
|
|
|
|
}
|
2021-05-25 20:04:05 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @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();
|