ts: Refactor (#265)

This commit is contained in:
Armani Ferrante 2021-05-08 14:52:26 -07:00 committed by GitHub
parent c06ad679d3
commit 06b40b7811
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1118 additions and 1021 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@project-serum/anchor",
"version": "0.5.0",
"version": "0.5.1-beta.1",
"description": "Anchor client",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",

View File

@ -1,6 +1,6 @@
import camelCase from "camelcase";
import { snakeCase } from "snake-case";
import { Layout, seq } from "buffer-layout";
import { Layout } from "buffer-layout";
import * as sha256 from "js-sha256";
import * as borsh from "@project-serum/borsh";
import {

View File

@ -1,12 +1,12 @@
import BN from "bn.js";
import * as web3 from "@solana/web3.js";
import Provider, { NodeWallet as Wallet } from "./provider";
import { Program } from "./program";
import Coder from "./coder";
import { Idl } from "./idl";
import workspace from "./workspace";
import utils from "./utils";
import { ProgramAccount } from "./rpc";
import { Program } from "./program";
import { ProgramAccount } from "./program/namespace";
let _provider: Provider | null = null;

View File

@ -1,269 +0,0 @@
import { PublicKey } from "@solana/web3.js";
import { inflate } from "pako";
import Provider from "./provider";
import { RpcFactory } from "./rpc";
import { Idl, idlAddress, decodeIdlAccount } from "./idl";
import Coder, { eventDiscriminator } from "./coder";
import { Rpcs, Ixs, Txs, Accounts, State } from "./rpc";
import { getProvider } from "./";
import * as base64 from "base64-js";
import * as assert from "assert";
/**
* Program is the IDL deserialized representation of a Solana program.
*/
export class Program {
/**
* Address of the program.
*/
readonly programId: PublicKey;
/**
* IDL describing this program's interface.
*/
readonly idl: Idl;
/**
* Async functions to invoke instructions against a Solana priogram running
* on a cluster.
*/
readonly rpc: Rpcs;
/**
* Async functions to fetch deserialized program accounts from a cluster.
*/
readonly account: Accounts;
/**
* Functions to build `TransactionInstruction` objects.
*/
readonly instruction: Ixs;
/**
* Functions to build `Transaction` objects.
*/
readonly transaction: Txs;
/**
* Coder for serializing rpc requests.
*/
readonly coder: Coder;
/**
* Object with state account accessors and rpcs.
*/
readonly state: State;
/**
* Wallet and network provider.
*/
readonly provider: Provider;
public constructor(idl: Idl, programId: PublicKey, provider?: Provider) {
this.idl = idl;
this.programId = programId;
this.provider = provider ?? getProvider();
// Build the serializer.
const coder = new Coder(idl);
// Build the dynamic RPC functions.
const [rpcs, ixs, txs, accounts, state] = RpcFactory.build(
idl,
coder,
programId,
this.provider
);
this.rpc = rpcs;
this.instruction = ixs;
this.transaction = txs;
this.account = accounts;
this.coder = coder;
this.state = state;
}
/**
* Generates a Program client by fetching the IDL from chain.
*/
public static async at(programId: PublicKey, provider?: Provider) {
const idl = await Program.fetchIdl(programId, provider);
return new Program(idl, programId, provider);
}
/**
* Fetches an idl from the blockchain.
*/
public static async fetchIdl(programId: PublicKey, provider?: Provider) {
provider = provider ?? getProvider();
const address = await idlAddress(programId);
const accountInfo = await provider.connection.getAccountInfo(address);
// Chop off account discriminator.
let idlAccount = decodeIdlAccount(accountInfo.data.slice(8));
const inflatedIdl = inflate(idlAccount.data);
return JSON.parse(decodeUtf8(inflatedIdl));
}
/**
* Invokes the given callback everytime the given event is emitted.
*/
public addEventListener<T>(
eventName: string,
callback: (event: T, slot: number) => void
): Promise<void> {
// Values shared across log handlers.
const thisProgramStr = this.programId.toString();
const discriminator = eventDiscriminator(eventName);
const logStartIndex = "Program log: ".length;
// Handles logs when the current program being executing is *not* this.
const handleSystemLog = (log: string): [string | null, boolean] => {
// System component.
const logStart = log.split(":")[0];
// Recursive call.
if (logStart.startsWith(`Program ${this.programId.toString()} invoke`)) {
return [this.programId.toString(), false];
}
// Cpi call.
else if (logStart.includes("invoke")) {
return ["cpi", false]; // Any string will do.
} else {
// Did the program finish executing?
if (logStart.match(/^Program (.*) consumed .*$/g) !== null) {
return [null, true];
}
return [null, false];
}
};
// Handles logs from *this* program.
const handleProgramLog = (
log: string
): [T | null, string | null, boolean] => {
// This is a `msg!` log.
if (log.startsWith("Program log:")) {
const logStr = log.slice(logStartIndex);
const logArr = Buffer.from(base64.toByteArray(logStr));
const disc = logArr.slice(0, 8);
// Only deserialize if the discriminator implies a proper event.
let event = null;
if (disc.equals(discriminator)) {
event = this.coder.events.decode(eventName, logArr.slice(8));
}
return [event, null, false];
}
// System log.
else {
return [null, ...handleSystemLog(log)];
}
};
// Main log handler. Returns a three element array of the event, the
// next program that was invoked for CPI, and a boolean indicating if
// a program has completed execution (and thus should be popped off the
// execution stack).
const handleLog = (
execution: ExecutionContext,
log: string
): [T | null, string | null, boolean] => {
// Executing program is this program.
if (execution.program() === thisProgramStr) {
return handleProgramLog(log);
}
// Executing program is not this program.
else {
return [null, ...handleSystemLog(log)];
}
};
// Each log given, represents an array of messages emitted by
// a single transaction, which can execute many different programs across
// CPI boundaries. However, the subscription is only interested in the
// events emitted by *this* program. In achieving this, we keep track of the
// program execution context by parsing each log and looking for a CPI
// `invoke` call. If one exists, we know a new program is executing. So we
// push the programId onto a stack and switch the program context. This
// allows us to track, for a given log, which program was executing during
// its emission, thereby allowing us to know if a given log event was
// emitted by *this* program. If it was, then we parse the raw string and
// emit the event if the string matches the event being subscribed to.
//
// @ts-ignore
return this.provider.connection.onLogs(this.programId, (logs, ctx) => {
if (logs.err) {
console.error(logs);
return;
}
const logScanner = new LogScanner(logs.logs);
const execution = new ExecutionContext(logScanner.next() as string);
let log = logScanner.next();
while (log !== null) {
let [event, newProgram, didPop] = handleLog(execution, log);
if (event) {
callback(event, ctx.slot);
}
if (newProgram) {
execution.push(newProgram);
}
if (didPop) {
execution.pop();
}
log = logScanner.next();
}
});
}
public async removeEventListener(listener: number): Promise<void> {
// @ts-ignore
return this.provider.connection.removeOnLogsListener(listener);
}
}
// Stack frame execution context, allowing one to track what program is
// executing for a given log.
class ExecutionContext {
stack: string[];
constructor(log: string) {
// Assumes the first log in every transaction is an `invoke` log from the
// runtime.
const program = /^Program (.*) invoke.*$/g.exec(log)[1];
this.stack = [program];
}
program(): string {
assert.ok(this.stack.length > 0);
return this.stack[this.stack.length - 1];
}
push(newProgram: string) {
this.stack.push(newProgram);
}
pop() {
assert.ok(this.stack.length > 0);
this.stack.pop();
}
}
class LogScanner {
constructor(public logs: string[]) {}
next(): string | null {
if (this.logs.length === 0) {
return null;
}
let l = this.logs[0];
this.logs = this.logs.slice(1);
return l;
}
}
function decodeUtf8(array: Uint8Array): string {
const decoder =
typeof TextDecoder === "undefined"
? new (require("util").TextDecoder)("utf-8") // Node.
: new TextDecoder("utf-8"); // Browser.
return decoder.decode(array);
}

79
ts/src/program/common.ts Normal file
View File

@ -0,0 +1,79 @@
import EventEmitter from "eventemitter3";
import { Idl, IdlInstruction, IdlAccountItem, IdlStateMethod } from "../idl";
import { ProgramError } from "../error";
import { RpcAccounts } from "./context";
export type Subscription = {
listener: number;
ee: EventEmitter;
};
export function parseIdlErrors(idl: Idl): Map<number, string> {
const errors = new Map();
if (idl.errors) {
idl.errors.forEach((e) => {
let msg = e.msg ?? e.name;
errors.set(e.code, msg);
});
}
return errors;
}
// Allow either IdLInstruction or IdlStateMethod since the types share fields.
export function toInstruction(
idlIx: IdlInstruction | IdlStateMethod,
...args: any[]
) {
if (idlIx.args.length != args.length) {
throw new Error("Invalid argument length");
}
const ix: { [key: string]: any } = {};
let idx = 0;
idlIx.args.forEach((ixArg) => {
ix[ixArg.name] = args[idx];
idx += 1;
});
return ix;
}
// Throws error if any account required for the `ix` is not given.
export function validateAccounts(
ixAccounts: IdlAccountItem[],
accounts: RpcAccounts
) {
ixAccounts.forEach((acc) => {
// @ts-ignore
if (acc.accounts !== undefined) {
// @ts-ignore
validateAccounts(acc.accounts, accounts[acc.name]);
} else {
if (accounts[acc.name] === undefined) {
throw new Error(`Invalid arguments: ${acc.name} not provided.`);
}
}
});
}
export function translateError(
idlErrors: Map<number, string>,
err: any
): Error | null {
// TODO: don't rely on the error string. web3.js should preserve the error
// code information instead of giving us an untyped string.
let components = err.toString().split("custom program error: ");
if (components.length === 2) {
try {
const errorCode = parseInt(components[1]);
let errorMsg = idlErrors.get(errorCode);
if (errorMsg === undefined) {
// Unexpected error code so just throw the untranslated error.
return null;
}
return new ProgramError(errorCode, errorMsg);
} catch (parseErr) {
// Unable to parse the error. Just return the untranslated error.
return null;
}
}
}

57
ts/src/program/context.ts Normal file
View File

@ -0,0 +1,57 @@
import {
Account,
AccountMeta,
PublicKey,
ConfirmOptions,
TransactionInstruction,
} from "@solana/web3.js";
import { IdlInstruction } from "../idl";
/**
* RpcContext provides all arguments for an RPC/IX invocation that are not
* covered by the instruction enum.
*/
type RpcContext = {
// Accounts the instruction will use.
accounts?: RpcAccounts;
// All accounts to pass into an instruction *after* the main `accounts`.
remainingAccounts?: AccountMeta[];
// Accounts that must sign the transaction.
signers?: Array<Account>;
// Instructions to run *before* the specified rpc instruction.
instructions?: TransactionInstruction[];
// RpcOptions.
options?: RpcOptions;
// Private namespace for dev.
__private?: { logAccounts: boolean };
};
/**
* Dynamic object representing a set of accounts given to an rpc/ix invocation.
* The name of each key should match the name for that account in the IDL.
*/
export type RpcAccounts = {
[key: string]: PublicKey | RpcAccounts;
};
/**
* Options for an RPC invocation.
*/
export type RpcOptions = ConfirmOptions;
export function splitArgsAndCtx(
idlIx: IdlInstruction,
args: any[]
): [any[], RpcContext] {
let options = {};
const inputLen = idlIx.args ? idlIx.args.length : 0;
if (args.length > inputLen) {
if (args.length !== inputLen + 1) {
throw new Error("provided too many arguments ${args}");
}
options = args.pop();
}
return [args, options];
}

149
ts/src/program/event.ts Normal file
View File

@ -0,0 +1,149 @@
import { PublicKey } from "@solana/web3.js";
import * as base64 from "base64-js";
import * as assert from "assert";
import Coder, { eventDiscriminator } from "../coder";
const LOG_START_INDEX = "Program log: ".length;
export class EventParser<T> {
private coder: Coder;
private programId: PublicKey;
private eventName: string;
private discriminator: Buffer;
constructor(coder: Coder, programId: PublicKey, eventName: string) {
this.coder = coder;
this.programId = programId;
this.eventName = eventName;
this.discriminator = eventDiscriminator(eventName);
}
// Each log given, represents an array of messages emitted by
// a single transaction, which can execute many different programs across
// CPI boundaries. However, the subscription is only interested in the
// events emitted by *this* program. In achieving this, we keep track of the
// program execution context by parsing each log and looking for a CPI
// `invoke` call. If one exists, we know a new program is executing. So we
// push the programId onto a stack and switch the program context. This
// allows us to track, for a given log, which program was executing during
// its emission, thereby allowing us to know if a given log event was
// emitted by *this* program. If it was, then we parse the raw string and
// emit the event if the string matches the event being subscribed to.
public parseLogs(logs: string[], callback: (log: T) => void) {
const logScanner = new LogScanner(logs);
const execution = new ExecutionContext(logScanner.next() as string);
let log = logScanner.next();
while (log !== null) {
let [event, newProgram, didPop] = this.handleLog(execution, log);
if (event) {
callback(event);
}
if (newProgram) {
execution.push(newProgram);
}
if (didPop) {
execution.pop();
}
log = logScanner.next();
}
}
// Handles logs when the current program being executing is *not* this.
private handleSystemLog(log: string): [string | null, boolean] {
// System component.
const logStart = log.split(":")[0];
// Recursive call.
if (logStart.startsWith(`Program ${this.programId.toString()} invoke`)) {
return [this.programId.toString(), false];
}
// Cpi call.
else if (logStart.includes("invoke")) {
return ["cpi", false]; // Any string will do.
} else {
// Did the program finish executing?
if (logStart.match(/^Program (.*) consumed .*$/g) !== null) {
return [null, true];
}
return [null, false];
}
}
// Handles logs from *this* program.
private handleProgramLog(log: string): [T | null, string | null, boolean] {
// This is a `msg!` log.
if (log.startsWith("Program log:")) {
const logStr = log.slice(LOG_START_INDEX);
const logArr = Buffer.from(base64.toByteArray(logStr));
const disc = logArr.slice(0, 8);
// Only deserialize if the discriminator implies a proper event.
let event = null;
if (disc.equals(this.discriminator)) {
event = this.coder.events.decode(this.eventName, logArr.slice(8));
}
return [event, null, false];
}
// System log.
else {
return [null, ...this.handleSystemLog(log)];
}
}
// Main log handler. Returns a three element array of the event, the
// next program that was invoked for CPI, and a boolean indicating if
// a program has completed execution (and thus should be popped off the
// execution stack).
private handleLog(
execution: ExecutionContext,
log: string
): [T | null, string | null, boolean] {
// Executing program is this program.
if (execution.program() === this.programId.toString()) {
return this.handleProgramLog(log);
}
// Executing program is not this program.
else {
return [null, ...this.handleSystemLog(log)];
}
}
}
// Stack frame execution context, allowing one to track what program is
// executing for a given log.
class ExecutionContext {
stack: string[];
constructor(log: string) {
// Assumes the first log in every transaction is an `invoke` log from the
// runtime.
const program = /^Program (.*) invoke.*$/g.exec(log)[1];
this.stack = [program];
}
program(): string {
assert.ok(this.stack.length > 0);
return this.stack[this.stack.length - 1];
}
push(newProgram: string) {
this.stack.push(newProgram);
}
pop() {
assert.ok(this.stack.length > 0);
this.stack.pop();
}
}
class LogScanner {
constructor(public logs: string[]) {}
next(): string | null {
if (this.logs.length === 0) {
return null;
}
let l = this.logs[0];
this.logs = this.logs.slice(1);
return l;
}
}

131
ts/src/program/index.ts Normal file
View File

@ -0,0 +1,131 @@
import { inflate } from "pako";
import { PublicKey } from "@solana/web3.js";
import Provider from "../provider";
import { Idl, idlAddress, decodeIdlAccount } from "../idl";
import Coder from "../coder";
import NamespaceFactory, { Rpcs, Ixs, Txs, Accounts, State } from "./namespace";
import { getProvider } from "../";
import { decodeUtf8 } from "../utils";
import { EventParser } from "./event";
/**
* Program is the IDL deserialized representation of a Solana program.
*/
export class Program {
/**
* Address of the program.
*/
readonly programId: PublicKey;
/**
* IDL describing this program's interface.
*/
readonly idl: Idl;
/**
* Async functions to invoke instructions against a Solana priogram running
* on a cluster.
*/
readonly rpc: Rpcs;
/**
* Async functions to fetch deserialized program accounts from a cluster.
*/
readonly account: Accounts;
/**
* Functions to build `TransactionInstruction` objects.
*/
readonly instruction: Ixs;
/**
* Functions to build `Transaction` objects.
*/
readonly transaction: Txs;
/**
* Coder for serializing rpc requests.
*/
readonly coder: Coder;
/**
* Object with state account accessors and rpcs.
*/
readonly state: State;
/**
* Wallet and network provider.
*/
readonly provider: Provider;
public constructor(idl: Idl, programId: PublicKey, provider?: Provider) {
this.idl = idl;
this.programId = programId;
this.provider = provider ?? getProvider();
// Build the serializer.
const coder = new Coder(idl);
// Build the dynamic namespaces.
const [rpcs, ixs, txs, accounts, state] = NamespaceFactory.build(
idl,
coder,
programId,
this.provider
);
this.rpc = rpcs;
this.instruction = ixs;
this.transaction = txs;
this.account = accounts;
this.coder = coder;
this.state = state;
}
/**
* Generates a Program client by fetching the IDL from chain.
*/
public static async at(programId: PublicKey, provider?: Provider) {
const idl = await Program.fetchIdl(programId, provider);
return new Program(idl, programId, provider);
}
/**
* Fetches an idl from the blockchain.
*/
public static async fetchIdl(programId: PublicKey, provider?: Provider) {
provider = provider ?? getProvider();
const address = await idlAddress(programId);
const accountInfo = await provider.connection.getAccountInfo(address);
// Chop off account discriminator.
let idlAccount = decodeIdlAccount(accountInfo.data.slice(8));
const inflatedIdl = inflate(idlAccount.data);
return JSON.parse(decodeUtf8(inflatedIdl));
}
/**
* Invokes the given callback everytime the given event is emitted.
*/
public addEventListener<T>(
eventName: string,
callback: (event: T, slot: number) => void
): number {
const eventParser = new EventParser<T>(
this.coder,
this.programId,
eventName
);
return this.provider.connection.onLogs(this.programId, (logs, ctx) => {
if (logs.err) {
console.error(logs);
return;
}
eventParser.parseLogs(logs.logs, (event) => {
callback(event, ctx.slot);
});
});
}
public async removeEventListener(listener: number): Promise<void> {
return this.provider.connection.removeOnLogsListener(listener);
}
}

View File

@ -0,0 +1,225 @@
import camelCase from "camelcase";
import EventEmitter from "eventemitter3";
import * as bs58 from "bs58";
import {
Account,
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.
*/
export interface Accounts {
[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: (account: Account) => Promise<TransactionInstruction>;
associated: (...args: PublicKey[]) => Promise<any>;
associatedAddress: (...args: PublicKey[]) => Promise<PublicKey>;
};
/**
* 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();
export default class AccountNamespace {
// Returns the generated accounts namespace.
public static build(
idl: Idl,
coder: Coder,
programId: PublicKey,
provider: Provider
): Accounts {
const accountFns: Accounts = {};
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 (
account: Account,
sizeOverride?: number
): Promise<TransactionInstruction> => {
// @ts-ignore
const size = accountsNamespace["size"];
return SystemProgram.createAccount({
fromPubkey: provider.wallet.publicKey,
newAccountPubkey: account.publicKey,
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;
}
}

View File

@ -0,0 +1,63 @@
import camelCase from "camelcase";
import { PublicKey } from "@solana/web3.js";
import Coder from "../../coder";
import Provider from "../../provider";
import { Idl } from "../../idl";
import { parseIdlErrors } from "../common";
import StateNamespace, { State } from "./state";
import InstructionNamespace, { Ixs } from "./instruction";
import TransactionNamespace, { Txs } from "./transaction";
import RpcNamespace, { Rpcs } from "./rpc";
import AccountNamespace, { Accounts } from "./account";
// Re-exports.
export { State } from "./state";
export { Ixs } from "./instruction";
export { Txs, TxFn } from "./transaction";
export { Rpcs, RpcFn } from "./rpc";
export { Accounts, AccountFn, ProgramAccount } from "./account";
export default class NamespaceFactory {
/**
* build dynamically generates RPC methods.
*
* @returns an object with all the RPC methods attached.
*/
public static build(
idl: Idl,
coder: Coder,
programId: PublicKey,
provider: Provider
): [Rpcs, Ixs, Txs, Accounts, State] {
const idlErrors = parseIdlErrors(idl);
const rpcs: Rpcs = {};
const ixFns: Ixs = {};
const txFns: Txs = {};
const state = StateNamespace.build(
idl,
coder,
programId,
idlErrors,
provider
);
idl.instructions.forEach((idlIx) => {
const ix = InstructionNamespace.build(idlIx, coder, programId);
const tx = TransactionNamespace.build(idlIx, ix);
const rpc = RpcNamespace.build(idlIx, tx, idlErrors, provider);
const name = camelCase(idlIx.name);
ixFns[name] = ix;
txFns[name] = tx;
rpcs[name] = rpc;
});
const accountFns = idl.accounts
? AccountNamespace.build(idl, coder, programId, provider)
: {};
return [rpcs, ixFns, txFns, accountFns, state];
}
}

View File

@ -0,0 +1,100 @@
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
import { IdlAccount, IdlInstruction, IdlAccountItem } from "../../idl";
import { IdlError } from "../../error";
import Coder from "../../coder";
import { toInstruction, validateAccounts } from "../common";
import { RpcAccounts, splitArgsAndCtx } from "../context";
/**
* Dynamically generated instruction namespace.
*/
export interface Ixs {
[key: string]: IxFn;
}
/**
* Ix is a function to create a `TransactionInstruction` generated from an IDL.
*/
export type IxFn = IxProps & ((...args: any[]) => any);
type IxProps = {
accounts: (ctx: RpcAccounts) => any;
};
export default class InstructionNamespace {
// Builds the instuction namespace.
public static build(
idlIx: IdlInstruction,
coder: Coder,
programId: PublicKey
): IxFn {
if (idlIx.name === "_inner") {
throw new IdlError("the _inner name is reserved");
}
const ix = (...args: any[]): TransactionInstruction => {
const [ixArgs, ctx] = splitArgsAndCtx(idlIx, [...args]);
validateAccounts(idlIx.accounts, ctx.accounts);
validateInstruction(idlIx, ...args);
const keys = InstructionNamespace.accountsArray(
ctx.accounts,
idlIx.accounts
);
if (ctx.remainingAccounts !== undefined) {
keys.push(...ctx.remainingAccounts);
}
if (ctx.__private && ctx.__private.logAccounts) {
console.log("Outgoing account metas:", keys);
}
return new TransactionInstruction({
keys,
programId,
data: coder.instruction.encode(
idlIx.name,
toInstruction(idlIx, ...ixArgs)
),
});
};
// Utility fn for ordering the accounts for this instruction.
ix["accounts"] = (accs: RpcAccounts) => {
return InstructionNamespace.accountsArray(accs, idlIx.accounts);
};
return ix;
}
public static accountsArray(
ctx: RpcAccounts,
accounts: IdlAccountItem[]
): any {
return accounts
.map((acc: IdlAccountItem) => {
// Nested accounts.
// @ts-ignore
const nestedAccounts: IdlAccountItem[] | undefined = acc.accounts;
if (nestedAccounts !== undefined) {
const rpcAccs = ctx[acc.name] as RpcAccounts;
return InstructionNamespace.accountsArray(
rpcAccs,
nestedAccounts
).flat();
} else {
const account: IdlAccount = acc as IdlAccount;
return {
pubkey: ctx[acc.name],
isWritable: account.isMut,
isSigner: account.isSigner,
};
}
})
.flat();
}
}
// Throws error if any argument required for the `ix` is not given.
function validateInstruction(ix: IdlInstruction, ...args: any[]) {
// todo
}

View File

@ -0,0 +1,46 @@
import { TransactionSignature } from "@solana/web3.js";
import Provider from "../../provider";
import { IdlInstruction } from "../../idl";
import { translateError } from "../common";
import { splitArgsAndCtx } from "../context";
import { TxFn } from "./transaction";
/**
* Dynamically generated rpc namespace.
*/
export interface Rpcs {
[key: string]: RpcFn;
}
/**
* RpcFn is a single rpc method generated from an IDL.
*/
export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;
export default class RpcNamespace {
// Builds the rpc namespace.
public static build(
idlIx: IdlInstruction,
txFn: TxFn,
idlErrors: Map<number, string>,
provider: Provider
): RpcFn {
const rpc = async (...args: any[]): Promise<TransactionSignature> => {
const tx = txFn(...args);
const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
try {
const txSig = await provider.send(tx, ctx.signers, ctx.options);
return txSig;
} catch (err) {
console.log("Translating error", err);
let translatedErr = translateError(idlErrors, err);
if (translatedErr === null) {
throw err;
}
throw translatedErr;
}
};
return rpc;
}
}

View File

@ -0,0 +1,223 @@
import EventEmitter from "eventemitter3";
import {
PublicKey,
SystemProgram,
Transaction,
TransactionSignature,
TransactionInstruction,
SYSVAR_RENT_PUBKEY,
Commitment,
} from "@solana/web3.js";
import Provider from "../../provider";
import { Idl, IdlStateMethod } from "../../idl";
import Coder, { stateDiscriminator } from "../../coder";
import { Rpcs, Ixs } from "./";
import {
Subscription,
translateError,
toInstruction,
validateAccounts,
} from "../common";
import { RpcAccounts, splitArgsAndCtx } from "../context";
import InstructionNamespace from "./instruction";
export type State = () =>
| Promise<any>
| {
address: () => Promise<PublicKey>;
rpc: Rpcs;
instruction: Ixs;
subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter;
unsubscribe: (address: PublicKey) => void;
};
export default class StateNamespace {
// Builds the state namespace.
public static build(
idl: Idl,
coder: Coder,
programId: PublicKey,
idlErrors: Map<number, string>,
provider: Provider
): State | undefined {
if (idl.state === undefined) {
return undefined;
}
// Fetches the state object from the blockchain.
const state = async (): Promise<any> => {
const addr = await programStateAddress(programId);
const accountInfo = await provider.connection.getAccountInfo(addr);
if (accountInfo === null) {
throw new Error(`Account does not exist ${addr.toString()}`);
}
// Assert the account discriminator is correct.
const expectedDiscriminator = await stateDiscriminator(
idl.state.struct.name
);
if (expectedDiscriminator.compare(accountInfo.data.slice(0, 8))) {
throw new Error("Invalid account discriminator");
}
return coder.state.decode(accountInfo.data);
};
// Namespace with all rpc functions.
const rpc: Rpcs = {};
const ix: Ixs = {};
idl.state.methods.forEach((m: IdlStateMethod) => {
const accounts = async (accounts: RpcAccounts): Promise<any> => {
const keys = await stateInstructionKeys(
programId,
provider,
m,
accounts
);
return keys.concat(
InstructionNamespace.accountsArray(accounts, m.accounts)
);
};
const ixFn = async (...args: any[]): Promise<TransactionInstruction> => {
const [ixArgs, ctx] = splitArgsAndCtx(m, [...args]);
return new TransactionInstruction({
keys: await accounts(ctx.accounts),
programId,
data: coder.instruction.encodeState(
m.name,
toInstruction(m, ...ixArgs)
),
});
};
ixFn["accounts"] = accounts;
ix[m.name] = ixFn;
rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
const [_, ctx] = splitArgsAndCtx(m, [...args]);
const tx = new Transaction();
if (ctx.instructions !== undefined) {
tx.add(...ctx.instructions);
}
tx.add(await ix[m.name](...args));
try {
const txSig = await provider.send(tx, ctx.signers, ctx.options);
return txSig;
} catch (err) {
let translatedErr = translateError(idlErrors, err);
if (translatedErr === null) {
throw err;
}
throw translatedErr;
}
};
});
state["rpc"] = rpc;
state["instruction"] = ix;
// Calculates the address of the program's global state object account.
state["address"] = async (): Promise<PublicKey> =>
programStateAddress(programId);
// Subscription singleton.
let sub: null | Subscription = null;
// Subscribe to account changes.
state["subscribe"] = (commitment?: Commitment): EventEmitter => {
if (sub !== null) {
return sub.ee;
}
const ee = new EventEmitter();
state["address"]().then((address) => {
const listener = provider.connection.onAccountChange(
address,
(acc) => {
const account = coder.state.decode(acc.data);
ee.emit("change", account);
},
commitment
);
sub = {
ee,
listener,
};
});
return ee;
};
// Unsubscribe from account changes.
state["unsubscribe"] = () => {
if (sub !== null) {
provider.connection
.removeAccountChangeListener(sub.listener)
.then(async () => {
sub = null;
})
.catch(console.error);
}
};
return state;
}
}
// Calculates the deterministic address of the program's "state" account.
async function programStateAddress(programId: PublicKey): Promise<PublicKey> {
let [registrySigner, _nonce] = await PublicKey.findProgramAddress(
[],
programId
);
return PublicKey.createWithSeed(registrySigner, "unversioned", programId);
}
// Returns the common keys that are prepended to all instructions targeting
// the "state" of a program.
async function stateInstructionKeys(
programId: PublicKey,
provider: Provider,
m: IdlStateMethod,
accounts: RpcAccounts
) {
if (m.name === "new") {
// Ctor `new` method.
const [programSigner, _nonce] = await PublicKey.findProgramAddress(
[],
programId
);
return [
{
pubkey: provider.wallet.publicKey,
isWritable: false,
isSigner: true,
},
{
pubkey: await programStateAddress(programId),
isWritable: true,
isSigner: false,
},
{ pubkey: programSigner, isWritable: false, isSigner: false },
{
pubkey: SystemProgram.programId,
isWritable: false,
isSigner: false,
},
{ pubkey: programId, isWritable: false, isSigner: false },
{
pubkey: SYSVAR_RENT_PUBKEY,
isWritable: false,
isSigner: false,
},
];
} else {
validateAccounts(m.accounts, accounts);
return [
{
pubkey: await programStateAddress(programId),
isWritable: true,
isSigner: false,
},
];
}
}

View File

@ -0,0 +1,33 @@
import { Transaction } from "@solana/web3.js";
import { IdlInstruction } from "../../idl";
import { splitArgsAndCtx } from "../context";
import { IxFn } from "./instruction";
/**
* Dynamically generated transaction namespace.
*/
export interface Txs {
[key: string]: TxFn;
}
/**
* Tx is a function to create a `Transaction` generate from an IDL.
*/
export type TxFn = (...args: any[]) => Transaction;
export default class TransactionNamespace {
// Builds the transaction namespace.
public static build(idlIx: IdlInstruction, ixFn: IxFn): TxFn {
const txFn = (...args: any[]): Transaction => {
const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
const tx = new Transaction();
if (ctx.instructions !== undefined) {
tx.add(...ctx.instructions);
}
tx.add(ixFn(...args));
return tx;
};
return txFn;
}
}

View File

@ -1,748 +0,0 @@
import camelCase from "camelcase";
import EventEmitter from "eventemitter3";
import * as bs58 from "bs58";
import {
Account,
AccountMeta,
PublicKey,
ConfirmOptions,
SystemProgram,
Transaction,
TransactionSignature,
TransactionInstruction,
SYSVAR_RENT_PUBKEY,
Commitment,
} from "@solana/web3.js";
import Provider from "./provider";
import {
Idl,
IdlAccount,
IdlInstruction,
IdlAccountItem,
IdlStateMethod,
} from "./idl";
import { IdlError, ProgramError } from "./error";
import Coder, {
ACCOUNT_DISCRIMINATOR_SIZE,
accountDiscriminator,
stateDiscriminator,
accountSize,
} from "./coder";
/**
* Dynamically generated rpc namespace.
*/
export interface Rpcs {
[key: string]: RpcFn;
}
/**
* Dynamically generated instruction namespace.
*/
export interface Ixs {
[key: string]: IxFn;
}
/**
* Dynamically generated transaction namespace.
*/
export interface Txs {
[key: string]: TxFn;
}
/**
* Accounts is a dynamically generated object to fetch any given account
* of a program.
*/
export interface Accounts {
[key: string]: AccountFn;
}
/**
* RpcFn is a single rpc method generated from an IDL.
*/
export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;
/**
* Ix is a function to create a `TransactionInstruction` generated from an IDL.
*/
export type IxFn = IxProps & ((...args: any[]) => any);
type IxProps = {
accounts: (ctx: RpcAccounts) => any;
};
/**
* Tx is a function to create a `Transaction` generate from an IDL.
*/
export type TxFn = (...args: any[]) => Transaction;
/**
* Account is a function returning a deserialized account, given an address.
*/
export type AccountFn<T = any> = AccountProps & ((address: PublicKey) => T);
/**
* Deserialized account owned by a program.
*/
export type ProgramAccount<T = any> = {
publicKey: PublicKey;
account: 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: (account: Account) => Promise<TransactionInstruction>;
associated: (...args: PublicKey[]) => Promise<any>;
associatedAddress: (...args: PublicKey[]) => Promise<PublicKey>;
};
/**
* Options for an RPC invocation.
*/
export type RpcOptions = ConfirmOptions;
/**
* RpcContext provides all arguments for an RPC/IX invocation that are not
* covered by the instruction enum.
*/
type RpcContext = {
// Accounts the instruction will use.
accounts?: RpcAccounts;
remainingAccounts?: AccountMeta[];
// Instructions to run *before* the specified rpc instruction.
instructions?: TransactionInstruction[];
// Accounts that must sign the transaction.
signers?: Array<Account>;
// RpcOptions.
options?: RpcOptions;
__private?: { logAccounts: boolean };
};
/**
* Dynamic object representing a set of accounts given to an rpc/ix invocation.
* The name of each key should match the name for that account in the IDL.
*/
type RpcAccounts = {
[key: string]: PublicKey | RpcAccounts;
};
export type State = () =>
| Promise<any>
| {
address: () => Promise<PublicKey>;
rpc: Rpcs;
instruction: Ixs;
subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter;
unsubscribe: (address: PublicKey) => void;
};
// Tracks all subscriptions.
const subscriptions: Map<string, Subscription> = new Map();
/**
* RpcFactory builds an Rpcs object for a given IDL.
*/
export class RpcFactory {
/**
* build dynamically generates RPC methods.
*
* @returns an object with all the RPC methods attached.
*/
public static build(
idl: Idl,
coder: Coder,
programId: PublicKey,
provider: Provider
): [Rpcs, Ixs, Txs, Accounts, State] {
const idlErrors = parseIdlErrors(idl);
const rpcs: Rpcs = {};
const ixFns: Ixs = {};
const txFns: Txs = {};
const state = RpcFactory.buildState(
idl,
coder,
programId,
idlErrors,
provider
);
idl.instructions.forEach((idlIx) => {
const name = camelCase(idlIx.name);
// Function to create a raw `TransactionInstruction`.
const ix = RpcFactory.buildIx(idlIx, coder, programId);
// Ffnction to create a `Transaction`.
const tx = RpcFactory.buildTx(idlIx, ix);
// Function to invoke an RPC against a cluster.
const rpc = RpcFactory.buildRpc(idlIx, tx, idlErrors, provider);
rpcs[name] = rpc;
ixFns[name] = ix;
txFns[name] = tx;
});
const accountFns = idl.accounts
? RpcFactory.buildAccounts(idl, coder, programId, provider)
: {};
return [rpcs, ixFns, txFns, accountFns, state];
}
// Builds the state namespace.
private static buildState(
idl: Idl,
coder: Coder,
programId: PublicKey,
idlErrors: Map<number, string>,
provider: Provider
): State | undefined {
if (idl.state === undefined) {
return undefined;
}
// Fetches the state object from the blockchain.
const state = async (): Promise<any> => {
const addr = await programStateAddress(programId);
const accountInfo = await provider.connection.getAccountInfo(addr);
if (accountInfo === null) {
throw new Error(`Account does not exist ${addr.toString()}`);
}
// Assert the account discriminator is correct.
const expectedDiscriminator = await stateDiscriminator(
idl.state.struct.name
);
if (expectedDiscriminator.compare(accountInfo.data.slice(0, 8))) {
throw new Error("Invalid account discriminator");
}
return coder.state.decode(accountInfo.data);
};
// Namespace with all rpc functions.
const rpc: Rpcs = {};
const ix: Ixs = {};
idl.state.methods.forEach((m: IdlStateMethod) => {
const accounts = async (accounts: RpcAccounts): Promise<any> => {
const keys = await stateInstructionKeys(
programId,
provider,
m,
accounts
);
return keys.concat(RpcFactory.accountsArray(accounts, m.accounts));
};
const ixFn = async (...args: any[]): Promise<TransactionInstruction> => {
const [ixArgs, ctx] = splitArgsAndCtx(m, [...args]);
return new TransactionInstruction({
keys: await accounts(ctx.accounts),
programId,
data: coder.instruction.encodeState(
m.name,
toInstruction(m, ...ixArgs)
),
});
};
ixFn["accounts"] = accounts;
ix[m.name] = ixFn;
rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
const [_, ctx] = splitArgsAndCtx(m, [...args]);
const tx = new Transaction();
if (ctx.instructions !== undefined) {
tx.add(...ctx.instructions);
}
tx.add(await ix[m.name](...args));
try {
const txSig = await provider.send(tx, ctx.signers, ctx.options);
return txSig;
} catch (err) {
let translatedErr = translateError(idlErrors, err);
if (translatedErr === null) {
throw err;
}
throw translatedErr;
}
};
});
state["rpc"] = rpc;
state["instruction"] = ix;
// Calculates the address of the program's global state object account.
state["address"] = async (): Promise<PublicKey> =>
programStateAddress(programId);
// Subscription singleton.
let sub: null | Subscription = null;
// Subscribe to account changes.
state["subscribe"] = (commitment?: Commitment): EventEmitter => {
if (sub !== null) {
return sub.ee;
}
const ee = new EventEmitter();
state["address"]().then((address) => {
const listener = provider.connection.onAccountChange(
address,
(acc) => {
const account = coder.state.decode(acc.data);
ee.emit("change", account);
},
commitment
);
sub = {
ee,
listener,
};
});
return ee;
};
// Unsubscribe from account changes.
state["unsubscribe"] = () => {
if (sub !== null) {
provider.connection
.removeAccountChangeListener(sub.listener)
.then(async () => {
sub = null;
})
.catch(console.error);
}
};
return state;
}
// Builds the instuction namespace.
private static buildIx(
idlIx: IdlInstruction,
coder: Coder,
programId: PublicKey
): IxFn {
if (idlIx.name === "_inner") {
throw new IdlError("the _inner name is reserved");
}
const ix = (...args: any[]): TransactionInstruction => {
const [ixArgs, ctx] = splitArgsAndCtx(idlIx, [...args]);
validateAccounts(idlIx.accounts, ctx.accounts);
validateInstruction(idlIx, ...args);
const keys = RpcFactory.accountsArray(ctx.accounts, idlIx.accounts);
if (ctx.remainingAccounts !== undefined) {
keys.push(...ctx.remainingAccounts);
}
if (ctx.__private && ctx.__private.logAccounts) {
console.log("Outgoing account metas:", keys);
}
return new TransactionInstruction({
keys,
programId,
data: coder.instruction.encode(
idlIx.name,
toInstruction(idlIx, ...ixArgs)
),
});
};
// Utility fn for ordering the accounts for this instruction.
ix["accounts"] = (accs: RpcAccounts) => {
return RpcFactory.accountsArray(accs, idlIx.accounts);
};
return ix;
}
private static accountsArray(
ctx: RpcAccounts,
accounts: IdlAccountItem[]
): any {
return accounts
.map((acc: IdlAccountItem) => {
// Nested accounts.
// @ts-ignore
const nestedAccounts: IdlAccountItem[] | undefined = acc.accounts;
if (nestedAccounts !== undefined) {
const rpcAccs = ctx[acc.name] as RpcAccounts;
return RpcFactory.accountsArray(rpcAccs, nestedAccounts).flat();
} else {
const account: IdlAccount = acc as IdlAccount;
return {
pubkey: ctx[acc.name],
isWritable: account.isMut,
isSigner: account.isSigner,
};
}
})
.flat();
}
// Builds the rpc namespace.
private static buildRpc(
idlIx: IdlInstruction,
txFn: TxFn,
idlErrors: Map<number, string>,
provider: Provider
): RpcFn {
const rpc = async (...args: any[]): Promise<TransactionSignature> => {
const tx = txFn(...args);
const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
try {
const txSig = await provider.send(tx, ctx.signers, ctx.options);
return txSig;
} catch (err) {
console.log("Translating error", err);
let translatedErr = translateError(idlErrors, err);
if (translatedErr === null) {
throw err;
}
throw translatedErr;
}
};
return rpc;
}
// Builds the transaction namespace.
private static buildTx(idlIx: IdlInstruction, ixFn: IxFn): TxFn {
const txFn = (...args: any[]): Transaction => {
const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
const tx = new Transaction();
if (ctx.instructions !== undefined) {
tx.add(...ctx.instructions);
}
tx.add(ixFn(...args));
return tx;
};
return txFn;
}
// Returns the generated accounts namespace.
private static buildAccounts(
idl: Idl,
coder: Coder,
programId: PublicKey,
provider: Provider
): Accounts {
const accountFns: Accounts = {};
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 (
account: Account,
sizeOverride?: number
): Promise<TransactionInstruction> => {
// @ts-ignore
const size = accountsNamespace["size"];
return SystemProgram.createAccount({
fromPubkey: provider.wallet.publicKey,
newAccountPubkey: account.publicKey,
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;
}
}
type Subscription = {
listener: number;
ee: EventEmitter;
};
function translateError(
idlErrors: Map<number, string>,
err: any
): Error | null {
// TODO: don't rely on the error string. web3.js should preserve the error
// code information instead of giving us an untyped string.
let components = err.toString().split("custom program error: ");
if (components.length === 2) {
try {
const errorCode = parseInt(components[1]);
let errorMsg = idlErrors.get(errorCode);
if (errorMsg === undefined) {
// Unexpected error code so just throw the untranslated error.
return null;
}
return new ProgramError(errorCode, errorMsg);
} catch (parseErr) {
// Unable to parse the error. Just return the untranslated error.
return null;
}
}
}
function parseIdlErrors(idl: Idl): Map<number, string> {
const errors = new Map();
if (idl.errors) {
idl.errors.forEach((e) => {
let msg = e.msg ?? e.name;
errors.set(e.code, msg);
});
}
return errors;
}
function splitArgsAndCtx(
idlIx: IdlInstruction,
args: any[]
): [any[], RpcContext] {
let options = {};
const inputLen = idlIx.args ? idlIx.args.length : 0;
if (args.length > inputLen) {
if (args.length !== inputLen + 1) {
throw new Error("provided too many arguments ${args}");
}
options = args.pop();
}
return [args, options];
}
// Allow either IdLInstruction or IdlStateMethod since the types share fields.
function toInstruction(idlIx: IdlInstruction | IdlStateMethod, ...args: any[]) {
if (idlIx.args.length != args.length) {
throw new Error("Invalid argument length");
}
const ix: { [key: string]: any } = {};
let idx = 0;
idlIx.args.forEach((ixArg) => {
ix[ixArg.name] = args[idx];
idx += 1;
});
return ix;
}
// Throws error if any account required for the `ix` is not given.
function validateAccounts(ixAccounts: IdlAccountItem[], accounts: RpcAccounts) {
ixAccounts.forEach((acc) => {
// @ts-ignore
if (acc.accounts !== undefined) {
// @ts-ignore
validateAccounts(acc.accounts, accounts[acc.name]);
} else {
if (accounts[acc.name] === undefined) {
throw new Error(`Invalid arguments: ${acc.name} not provided.`);
}
}
});
}
// Throws error if any argument required for the `ix` is not given.
function validateInstruction(ix: IdlInstruction, ...args: any[]) {
// todo
}
// Calculates the deterministic address of the program's "state" account.
async function programStateAddress(programId: PublicKey): Promise<PublicKey> {
let [registrySigner, _nonce] = await PublicKey.findProgramAddress(
[],
programId
);
return PublicKey.createWithSeed(registrySigner, "unversioned", programId);
}
// Returns the common keys that are prepended to all instructions targeting
// the "state" of a program.
async function stateInstructionKeys(
programId: PublicKey,
provider: Provider,
m: IdlStateMethod,
accounts: RpcAccounts
) {
if (m.name === "new") {
// Ctor `new` method.
const [programSigner, _nonce] = await PublicKey.findProgramAddress(
[],
programId
);
return [
{
pubkey: provider.wallet.publicKey,
isWritable: false,
isSigner: true,
},
{
pubkey: await programStateAddress(programId),
isWritable: true,
isSigner: false,
},
{ pubkey: programSigner, isWritable: false, isSigner: false },
{
pubkey: SystemProgram.programId,
isWritable: false,
isSigner: false,
},
{ pubkey: programId, isWritable: false, isSigner: false },
{
pubkey: SYSVAR_RENT_PUBKEY,
isWritable: false,
isSigner: false,
},
];
} else {
validateAccounts(m.accounts, accounts);
return [
{
pubkey: await programStateAddress(programId),
isWritable: true,
isSigner: false,
},
];
}
}

View File

@ -69,6 +69,14 @@ async function getMultipleAccounts(
});
}
export function decodeUtf8(array: Uint8Array): string {
const decoder =
typeof TextDecoder === "undefined"
? new (require("util").TextDecoder)("utf-8") // Node.
: new TextDecoder("utf-8"); // Browser.
return decoder.decode(array);
}
const utils = {
bs58,
sha256,