Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Adds types to Anchor #537

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ jobs:
- <<: *examples
name: Runs the examples 3
script:
- pushd examples/escrow && yarn && anchor test && popd
- pushd ts && yarn && yarn build && npm link && popd
- pushd examples/escrow && npm link @project-serum/anchor && yarn && popd
- pushd examples/escrow && anchor build && npx ts-node createIDLType.ts && anchor test --skip-build && popd
- pushd examples/pyth && yarn && anchor test && popd
- pushd examples/tutorial/basic-0 && anchor test && popd
- pushd examples/tutorial/basic-1 && anchor test && popd
Expand Down
18 changes: 18 additions & 0 deletions examples/escrow/createIDLType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const fs = require('fs')
import camelcase from "camelcase";

fs.rmdirSync("tests/types", { recursive: true });
fs.mkdir("tests/types", { recursive: true }, (err) => {
if (err) {
throw err;
}
});

let escrowIDLJSON = JSON.parse(fs.readFileSync('./target/idl/escrow.json'));
for (let account of escrowIDLJSON.accounts) {
account.name = camelcase(account.name);
}

const fileContents = `export type EscrowIDL = ${JSON.stringify(escrowIDLJSON)};`;
fs.writeFileSync("tests/types/escrow.ts", fileContents);

16 changes: 9 additions & 7 deletions examples/escrow/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"dependencies": {
"@project-serum/anchor": "^0.9.0",
"@project-serum/serum": "0.13.38",
"@solana/web3.js": "^1.18.0",
"@solana/spl-token": "^0.1.6"
},
"devDependencies": {
"ts-mocha": "^8.0.0"
"@project-serum/anchor": "../../ts",
"@project-serum/serum": "latest",
"@solana/spl-token": "latest",
"@solana/web3.js": "latest",
"@types/mocha": "^8.2.3",
"bn.js": "^5.2.0",
"camelcase": "^6.2.0",
"@types/node": "^14.14.37",
"chai": "^4.3.4"
}
}
24 changes: 13 additions & 11 deletions examples/escrow/tests/escrow.js → examples/escrow/tests/escrow.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
const anchor = require("@project-serum/anchor");
const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token");
const assert = require("assert");
import * as anchor from "@project-serum/anchor";
import { TOKEN_PROGRAM_ID, Token } from "@solana/spl-token";
import {assert} from "chai";

import {EscrowIDL} from "./types/escrow";

describe("escrow", () => {
const provider = anchor.Provider.env();
anchor.setProvider(provider);

const program = anchor.workspace.Escrow;
const program = anchor.workspace.Escrow as anchor.Program<EscrowIDL>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm the account namespace doesn't type check. I can do program.account.notAValidType and it compiles.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this expected @macalinao or did I miss something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may have forgotten to add this in

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed this by tweaking the AccountNamespace. But for this to work, I also needed to customize the script that produces the idl type such that the Account names are camel case.

I think we'd want to think through the dev flow for producing the IDL type definition if we are to ship this. Maybe we add a script to anchor-cli that produces the IDL types for you?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting this on your radar @armaniferrante

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ready for another review/testing?


let mintA = null;
let mintB = null;
let initializerTokenAccountA = null;
let initializerTokenAccountB = null;
let takerTokenAccountA = null;
let takerTokenAccountB = null;
let pda = null;
let mintA : Token = null;
let mintB : Token = null;
let initializerTokenAccountA : anchor.web3.PublicKey = null;
let initializerTokenAccountB : anchor.web3.PublicKey = null;
let takerTokenAccountA : anchor.web3.PublicKey = null;
let takerTokenAccountB : anchor.web3.PublicKey = null;
let pda : anchor.web3.PublicKey = null;

const takerAmount = 1000;
const initializerAmount = 500;
Expand Down
1 change: 1 addition & 0 deletions examples/escrow/tests/types/escrow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type EscrowIDL = {"version":"0.0.0","name":"escrow","instructions":[{"name":"initializeEscrow","accounts":[{"name":"initializer","isMut":false,"isSigner":true},{"name":"initializerDepositTokenAccount","isMut":true,"isSigner":false},{"name":"initializerReceiveTokenAccount","isMut":false,"isSigner":false},{"name":"escrowAccount","isMut":true,"isSigner":false},{"name":"tokenProgram","isMut":false,"isSigner":false},{"name":"rent","isMut":false,"isSigner":false}],"args":[{"name":"initializerAmount","type":"u64"},{"name":"takerAmount","type":"u64"}]},{"name":"cancelEscrow","accounts":[{"name":"initializer","isMut":false,"isSigner":false},{"name":"pdaDepositTokenAccount","isMut":true,"isSigner":false},{"name":"pdaAccount","isMut":false,"isSigner":false},{"name":"escrowAccount","isMut":true,"isSigner":false},{"name":"tokenProgram","isMut":false,"isSigner":false}],"args":[]},{"name":"exchange","accounts":[{"name":"taker","isMut":false,"isSigner":true},{"name":"takerDepositTokenAccount","isMut":true,"isSigner":false},{"name":"takerReceiveTokenAccount","isMut":true,"isSigner":false},{"name":"pdaDepositTokenAccount","isMut":true,"isSigner":false},{"name":"initializerReceiveTokenAccount","isMut":true,"isSigner":false},{"name":"initializerMainAccount","isMut":true,"isSigner":false},{"name":"escrowAccount","isMut":true,"isSigner":false},{"name":"pdaAccount","isMut":false,"isSigner":false},{"name":"tokenProgram","isMut":false,"isSigner":false}],"args":[]}],"accounts":[{"name":"escrowAccount","type":{"kind":"struct","fields":[{"name":"initializerKey","type":"publicKey"},{"name":"initializerDepositTokenAccount","type":"publicKey"},{"name":"initializerReceiveTokenAccount","type":"publicKey"},{"name":"initializerAmount","type":"u64"},{"name":"takerAmount","type":"u64"}]}}],"metadata":{"address":"orwyukmpT9ZKxq78aQCkM75xJDgV1VXgAktFgX6PZob"}};
10 changes: 10 additions & 0 deletions examples/escrow/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"types": ["mocha", "chai", "node"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2015"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true
}
}
2 changes: 1 addition & 1 deletion ts/src/idl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ type IdlEnumFieldsNamed = IdlField[];

type IdlEnumFieldsTuple = IdlType[];

type IdlErrorCode = {
export type IdlErrorCode = {
code: number;
name: string;
msg?: string;
Expand Down
14 changes: 9 additions & 5 deletions ts/src/program/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import {
TransactionInstruction,
} from "@solana/web3.js";
import { Address } from "./common";
import { IdlInstruction } from "../idl";
import { IdlAccountItem, IdlAccounts, IdlInstruction } from "../idl";

/**
* Context provides all non-argument inputs for generating Anchor transactions.
*/
export type Context = {
export type Context<A extends Accounts = Accounts> = {
/**
* Accounts used in the instruction context.
*/
accounts?: Accounts;
accounts?: A;

/**
* All accounts to pass into an instruction *after* the main `accounts`.
Expand Down Expand Up @@ -55,10 +55,14 @@ export type Context = {
* If multiple accounts are nested in the rust program, then they should be
* nested here.
*/
export type Accounts = {
[key: string]: Address | Accounts;
export type Accounts<A extends IdlAccountItem = IdlAccountItem> = {
[N in A["name"]]: Account<A & { name: N }>;
};

type Account<A extends IdlAccountItem> = A extends IdlAccounts
? Accounts<A["accounts"][number]>
: Address;

export function splitArgsAndCtx(
idlIx: IdlInstruction,
args: any[]
Expand Down
15 changes: 12 additions & 3 deletions ts/src/program/event.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { PublicKey } from "@solana/web3.js";
import * as assert from "assert";
import { IdlEvent, IdlEventField } from "src/idl";
import Coder from "../coder";
import { DecodeType } from "./namespace/types";

const LOG_START_INDEX = "Program log: ".length;

// Deserialized event.
export type Event = {
name: string;
data: Object;
export type Event<
E extends IdlEvent = IdlEvent,
Defined = Record<string, never>
> = {
name: E["name"];
data: EventData<E["fields"][number], Defined>;
};

type EventData<T extends IdlEventField, Defined> = {
[N in T["name"]]: DecodeType<(T & { name: N })["type"], Defined>;
};

export class EventParser {
Expand Down
32 changes: 19 additions & 13 deletions ts/src/program/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { Address, translateAddress } from "./common";
* below will refer to the two counter examples found
* [here](https://github.com/project-serum/anchor#examples).
*/
export class Program {
export class Program<IDL extends Idl = Idl> {
/**
* Async methods to send signed transactions to *non*-state methods on the
* program, returning a [[TransactionSignature]].
Expand Down Expand Up @@ -73,7 +73,7 @@ export class Program {
* });
* ```
*/
readonly rpc: RpcNamespace;
readonly rpc: RpcNamespace<IDL, IDL["instructions"][number]>;

/**
* The namespace provides handles to an [[AccountClient]] object for each
Expand All @@ -95,7 +95,7 @@ export class Program {
*
* For the full API, see the [[AccountClient]] reference.
*/
readonly account: AccountNamespace;
readonly account: AccountNamespace<IDL>;

/**
* The namespace provides functions to build [[TransactionInstruction]]
Expand Down Expand Up @@ -126,7 +126,7 @@ export class Program {
* });
* ```
*/
readonly instruction: InstructionNamespace;
readonly instruction: InstructionNamespace<IDL, IDL["instructions"][number]>;

/**
* The namespace provides functions to build [[Transaction]] objects for each
Expand Down Expand Up @@ -157,7 +157,7 @@ export class Program {
* });
* ```
*/
readonly transaction: TransactionNamespace;
readonly transaction: TransactionNamespace<IDL, IDL["instructions"][number]>;

/**
* The namespace provides functions to simulate transactions for each method
Expand Down Expand Up @@ -193,14 +193,14 @@ export class Program {
* });
* ```
*/
readonly simulate: SimulateNamespace;
readonly simulate: SimulateNamespace<IDL, IDL["instructions"][number]>;

/**
* A client for the program state. Similar to the base [[Program]] client,
* one can use this to send transactions and read accounts for the state
* abstraction.
*/
readonly state: StateClient;
readonly state: StateClient<IDL>;

/**
* Address of the program.
Expand All @@ -213,10 +213,10 @@ export class Program {
/**
* IDL defining the program's interface.
*/
public get idl(): Idl {
public get idl(): IDL {
return this._idl;
}
private _idl: Idl;
private _idl: IDL;

/**
* Coder for serializing requests.
Expand All @@ -240,7 +240,7 @@ export class Program {
* @param provider The network and wallet context to use. If not provided
* then uses [[getProvider]].
*/
public constructor(idl: Idl, programId: Address, provider?: Provider) {
public constructor(idl: IDL, programId: Address, provider?: Provider) {
programId = translateAddress(programId);

// Fields.
Expand Down Expand Up @@ -275,10 +275,13 @@ export class Program {
* @param programId The on-chain address of the program.
* @param provider The network and wallet context.
*/
public static async at(address: Address, provider?: Provider) {
public static async at<IDL extends Idl = Idl>(
address: Address,
provider?: Provider
) {
const programId = translateAddress(address);

const idl = await Program.fetchIdl(programId, provider);
const idl = await Program.fetchIdl<IDL>(programId, provider);
return new Program(idl, programId, provider);
}

Expand All @@ -291,7 +294,10 @@ export class Program {
* @param programId The on-chain address of the program.
* @param provider The network and wallet context.
*/
public static async fetchIdl(address: Address, provider?: Provider) {
public static async fetchIdl<IDL extends Idl = Idl>(
address: Address,
provider?: Provider
): Promise<IDL> {
provider = provider ?? getProvider();
const programId = translateAddress(address);

Expand Down
40 changes: 24 additions & 16 deletions ts/src/program/namespace/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,21 @@ import Coder, {
} from "../../coder";
import { Subscription, Address, translateAddress } from "../common";
import { getProvider } from "../../";
import { AllAccountsMap, IdlTypes, TypeDef } from "./types";
import * as pubkeyUtil from "../../utils/pubkey";

export default class AccountFactory {
public static build(
idl: Idl,
public static build<IDL extends Idl>(
idl: IDL,
coder: Coder,
programId: PublicKey,
provider: Provider
): AccountNamespace {
): AccountNamespace<IDL> {
const accountFns: AccountNamespace = {};

idl.accounts.forEach((idlAccount) => {
const name = camelCase(idlAccount.name);
accountFns[name] = new AccountClient(
accountFns[name] = new AccountClient<IDL>(
idl,
idlAccount,
programId,
Expand All @@ -39,7 +40,7 @@ export default class AccountFactory {
);
});

return accountFns;
return accountFns as AccountNamespace<IDL>;
}
}

Expand All @@ -63,11 +64,15 @@ export default class AccountFactory {
*
* For the full API, see the [[AccountClient]] reference.
*/
export interface AccountNamespace {
[key: string]: AccountClient;
}
export type AccountNamespace<IDL extends Idl = Idl> = {
[M in keyof AllAccountsMap<IDL>]: AccountClient<IDL>;
};

export class AccountClient {
export class AccountClient<
IDL extends Idl = Idl,
A extends IDL["accounts"][number] = IDL["accounts"][number],
T = TypeDef<A, IdlTypes<IDL>>
> {
/**
* Returns the number of bytes in this account.
*/
Expand Down Expand Up @@ -100,11 +105,11 @@ export class AccountClient {
}
private _coder: Coder;

private _idlAccount: IdlTypeDef;
private _idlAccount: A;

constructor(
idl: Idl,
idlAccount: IdlTypeDef,
idl: IDL,
idlAccount: A,
programId: PublicKey,
provider?: Provider,
coder?: Coder
Expand All @@ -121,7 +126,7 @@ export class AccountClient {
*
* @param address The address of the account to fetch.
*/
async fetch(address: Address): Promise<Object> {
async fetch(address: Address): Promise<T> {
const accountInfo = await this._provider.connection.getAccountInfo(
translateAddress(address)
);
Expand All @@ -135,13 +140,16 @@ export class AccountClient {
throw new Error("Invalid account discriminator");
}

return this._coder.accounts.decode(this._idlAccount.name, accountInfo.data);
return this._coder.accounts.decode<T>(
this._idlAccount.name,
accountInfo.data
);
}

/**
* Returns all instances of this account type for the program.
*/
async all(filter?: Buffer): Promise<ProgramAccount<any>[]> {
async all(filter?: Buffer): Promise<ProgramAccount<T>[]> {
let bytes = await accountDiscriminator(this._idlAccount.name);
if (filter !== undefined) {
bytes = Buffer.concat([bytes, filter]);
Expand Down Expand Up @@ -246,7 +254,7 @@ export class AccountClient {
* Function returning the associated account. Args are keys to associate.
* Order matters.
*/
async associated(...args: Array<PublicKey | Buffer>): Promise<any> {
async associated(...args: Array<PublicKey | Buffer>): Promise<T> {
const addr = await this.associatedAddress(...args);
return await this.fetch(addr);
}
Expand Down
Loading