ts: Refactor (#265)
This commit is contained in:
parent
c06ad679d3
commit
06b40b7811
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
748
ts/src/rpc.ts
748
ts/src/rpc.ts
|
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue