ts: Reorganize program namespaces into well typed elements (#322)

This commit is contained in:
Armani Ferrante 2021-05-25 20:04:05 -07:00 committed by GitHub
parent e1229362bc
commit 2f780e0d27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1469 additions and 1117 deletions

View File

@ -19,9 +19,10 @@ incremented for features.
## Breaking Changes
* ts: Retrieving deserialized accounts from the `<program>.account.<my-account>` and `<program>.state` namespaces now require explicitly invoking the `fetch` API. For example, `program.account.myAccount(<adddress>)` and `program.state()` is now `program.account.myAccount.fetch(<address>)` and `program.state.fetch()` ([#322](https://github.com/project-serum/anchor/pull/322)).
* lang: `#[account(associated)]` now requires `init` to be provided to create an associated account. If not provided, then the address will be assumed to exist, and a constraint will be added to ensure its correctness ([#318](https://github.com/project-serum/anchor/pull/318)).
* lang, ts: Change account discriminator pre-image of the `#[state]` account discriminator to be namespaced by "state:". This change should only be noticed by library maintainers ([#320](https://github.com/project-serum/anchor/pull/320)).
* lang, ts: Change domain delimiters for the pre-image of the instruciton sighash to be a single colon `:` to be consistent with accounts. This change should only be noticed by library maintainers.
* lang, ts: Change account discriminator pre-image of the `#[state]` account discriminator to be namespaced by "state:" ([#320](https://github.com/project-serum/anchor/pull/320)).
* lang, ts: Change domain delimiters for the pre-image of the instruciton sighash to be a single colon `:` to be consistent with accounts ([#321](https://github.com/project-serum/anchor/pull/321)).
## [0.6.0] - 2021-05-23

View File

@ -63,7 +63,7 @@ describe("cashiers-check", () => {
],
});
const checkAccount = await program.account.check(check.publicKey);
const checkAccount = await program.account.check.fetch(check.publicKey);
assert.ok(checkAccount.from.equals(god));
assert.ok(checkAccount.to.equals(receiver));
assert.ok(checkAccount.amount.eq(new anchor.BN(100)));
@ -91,7 +91,7 @@ describe("cashiers-check", () => {
},
});
const checkAccount = await program.account.check(check.publicKey);
const checkAccount = await program.account.check.fetch(check.publicKey);
assert.ok(checkAccount.burned === true);
let vaultAccount = await serumCmn.getTokenAccount(

View File

@ -25,7 +25,7 @@ describe("chat", () => {
signers: [chatRoom],
});
const chat = await program.account.chatRoom(chatRoom.publicKey);
const chat = await program.account.chatRoom.fetch(chatRoom.publicKey);
const name = new TextDecoder("utf-8").decode(new Uint8Array(chat.name));
assert.ok(name.startsWith("Test Chat")); // [u8; 280] => trailing zeros.
assert.ok(chat.messages.length === 33607);
@ -76,7 +76,7 @@ describe("chat", () => {
}
// Check the chat room state is as expected.
const chat = await program.account.chatRoom(chatRoom.publicKey);
const chat = await program.account.chatRoom.fetch(chatRoom.publicKey);
const name = new TextDecoder("utf-8").decode(new Uint8Array(chat.name));
assert.ok(name.startsWith("Test Chat")); // [u8; 280] => trailing zeros.
assert.ok(chat.messages.length === 33607);

View File

@ -41,8 +41,8 @@ describe("composite", () => {
}
);
const dummyAAccount = await program.account.dummyA(dummyA.publicKey);
const dummyBAccount = await program.account.dummyB(dummyB.publicKey);
const dummyAAccount = await program.account.dummyA.fetch(dummyA.publicKey);
const dummyBAccount = await program.account.dummyB.fetch(dummyB.publicKey);
assert.ok(dummyAAccount.data.eq(new anchor.BN(1234)));
assert.ok(dummyBAccount.data.eq(new anchor.BN(4321)));

View File

@ -10,7 +10,7 @@ describe("interface", () => {
it("Is initialized!", async () => {
await counter.state.rpc.new(counterAuth.programId);
const stateAccount = await counter.state();
const stateAccount = await counter.state.fetch();
assert.ok(stateAccount.count.eq(new anchor.BN(0)));
assert.ok(stateAccount.authProgram.equals(counterAuth.programId));
});
@ -39,7 +39,7 @@ describe("interface", () => {
authProgram: counterAuth.programId,
},
});
const stateAccount = await counter.state();
const stateAccount = await counter.state.fetch();
assert.ok(stateAccount.count.eq(new anchor.BN(3)));
});
});

View File

@ -37,7 +37,7 @@ describe("Lockup and Registry", () => {
});
lockupAddress = await lockup.state.address();
const lockupAccount = await lockup.state();
const lockupAccount = await lockup.state.fetch();
assert.ok(lockupAccount.authority.equals(provider.wallet.publicKey));
assert.ok(lockupAccount.whitelist.length === WHITELIST_SIZE);
@ -63,7 +63,7 @@ describe("Lockup and Registry", () => {
},
});
let lockupAccount = await lockup.state();
let lockupAccount = await lockup.state.fetch();
assert.ok(lockupAccount.authority.equals(newAuthority.publicKey));
await lockup.state.rpc.setAuthority(provider.wallet.publicKey, {
@ -73,7 +73,7 @@ describe("Lockup and Registry", () => {
signers: [newAuthority],
});
lockupAccount = await lockup.state();
lockupAccount = await lockup.state.fetch();
assert.ok(lockupAccount.authority.equals(provider.wallet.publicKey));
});
@ -97,7 +97,7 @@ describe("Lockup and Registry", () => {
await lockup.state.rpc.whitelistAdd(entries[0], { accounts });
let lockupAccount = await lockup.state();
let lockupAccount = await lockup.state.fetch();
assert.ok(lockupAccount.whitelist.length === 1);
assert.deepEqual(lockupAccount.whitelist, [entries[0]]);
@ -106,7 +106,7 @@ describe("Lockup and Registry", () => {
await lockup.state.rpc.whitelistAdd(entries[k], { accounts });
}
lockupAccount = await lockup.state();
lockupAccount = await lockup.state.fetch();
assert.deepEqual(lockupAccount.whitelist, entries);
@ -129,7 +129,7 @@ describe("Lockup and Registry", () => {
authority: provider.wallet.publicKey,
},
});
let lockupAccount = await lockup.state();
let lockupAccount = await lockup.state.fetch();
assert.deepEqual(lockupAccount.whitelist, entries.slice(1));
});
@ -185,7 +185,7 @@ describe("Lockup and Registry", () => {
}
);
vestingAccount = await lockup.account.vesting(vesting.publicKey);
vestingAccount = await lockup.account.vesting.fetch(vesting.publicKey);
assert.ok(vestingAccount.beneficiary.equals(provider.wallet.publicKey));
assert.ok(vestingAccount.mint.equals(mint));
@ -246,7 +246,7 @@ describe("Lockup and Registry", () => {
},
});
vestingAccount = await lockup.account.vesting(vesting.publicKey);
vestingAccount = await lockup.account.vesting.fetch(vesting.publicKey);
assert.ok(vestingAccount.outstanding.eq(new anchor.BN(0)));
const vaultAccount = await serumCmn.getTokenAccount(
@ -287,7 +287,7 @@ describe("Lockup and Registry", () => {
accounts: { lockupProgram: lockup.programId },
});
const state = await registry.state();
const state = await registry.state.fetch();
assert.ok(state.lockupProgram.equals(lockup.programId));
// Should not allow a second initializatoin.
@ -324,7 +324,7 @@ describe("Lockup and Registry", () => {
}
);
registrarAccount = await registry.account.registrar(registrar.publicKey);
registrarAccount = await registry.account.registrar.fetch(registrar.publicKey);
assert.ok(registrarAccount.authority.equals(provider.wallet.publicKey));
assert.equal(registrarAccount.nonce, nonce);
@ -385,7 +385,7 @@ describe("Lockup and Registry", () => {
let txSigs = await provider.sendAll(allTxs);
memberAccount = await registry.account.member(member.publicKey);
memberAccount = await registry.account.member.fetch(member.publicKey);
assert.ok(memberAccount.registrar.equals(registrar.publicKey));
assert.ok(memberAccount.beneficiary.equals(provider.wallet.publicKey));
@ -516,7 +516,7 @@ describe("Lockup and Registry", () => {
}
);
const vendorAccount = await registry.account.rewardVendor(
const vendorAccount = await registry.account.rewardVendor.fetch(
unlockedVendor.publicKey
);
@ -531,7 +531,7 @@ describe("Lockup and Registry", () => {
assert.ok(vendorAccount.rewardEventQCursor === 0);
assert.deepEqual(vendorAccount.kind, rewardKind);
const rewardQAccount = await registry.account.rewardQueue(
const rewardQAccount = await registry.account.rewardQueue.fetch(
rewardQ.publicKey
);
assert.ok(rewardQAccount.head === 1);
@ -571,7 +571,7 @@ describe("Lockup and Registry", () => {
let tokenAccount = await serumCmn.getTokenAccount(provider, token);
assert.ok(tokenAccount.amount.eq(new anchor.BN(200)));
const memberAccount = await registry.account.member(member.publicKey);
const memberAccount = await registry.account.member.fetch(member.publicKey);
assert.ok(memberAccount.rewardsCursor == 1);
});
@ -635,7 +635,7 @@ describe("Lockup and Registry", () => {
}
);
const vendorAccount = await registry.account.rewardVendor(
const vendorAccount = await registry.account.rewardVendor.fetch(
lockedVendor.publicKey
);
@ -653,7 +653,7 @@ describe("Lockup and Registry", () => {
JSON.stringify(lockedRewardKind)
);
const rewardQAccount = await registry.account.rewardQueue(
const rewardQAccount = await registry.account.rewardQueue.fetch(
rewardQ.publicKey
);
assert.ok(rewardQAccount.head === 2);
@ -727,7 +727,7 @@ describe("Lockup and Registry", () => {
],
});
const lockupAccount = await lockup.account.vesting(
const lockupAccount = await lockup.account.vesting.fetch(
vendoredVesting.publicKey
);

View File

@ -11,7 +11,7 @@ describe("misc", () => {
it("Can allocate extra space for a state constructor", async () => {
const tx = await program.state.rpc.new();
const addr = await program.state.address();
const state = await program.state();
const state = await program.state.fetch();
const accountInfo = await program.provider.connection.getAccountInfo(addr);
assert.ok(state.v.equals(Buffer.from([])));
assert.ok(accountInfo.data.length === 99);
@ -32,7 +32,7 @@ describe("misc", () => {
instructions: [await program.account.data.createInstruction(data)],
}
);
const dataAccount = await program.account.data(data.publicKey);
const dataAccount = await program.account.data.fetch(data.publicKey);
assert.ok(dataAccount.udata.eq(new anchor.BN(1234)));
assert.ok(dataAccount.idata.eq(new anchor.BN(22)));
});
@ -47,7 +47,7 @@ describe("misc", () => {
signers: [data],
instructions: [await program.account.dataU16.createInstruction(data)],
});
const dataAccount = await program.account.dataU16(data.publicKey);
const dataAccount = await program.account.dataU16.fetch(data.publicKey);
assert.ok(dataAccount.data === 99);
});
@ -110,7 +110,7 @@ describe("misc", () => {
authority: program.provider.wallet.publicKey,
},
});
let stateAccount = await misc2Program.state();
let stateAccount = await misc2Program.state.fetch();
assert.ok(stateAccount.data.eq(oldData));
assert.ok(stateAccount.auth.equals(program.provider.wallet.publicKey));
const newData = new anchor.BN(2134);
@ -121,7 +121,7 @@ describe("misc", () => {
misc2Program: misc2Program.programId,
},
});
stateAccount = await misc2Program.state();
stateAccount = await misc2Program.state.fetch();
assert.ok(stateAccount.data.eq(newData));
assert.ok(stateAccount.auth.equals(program.provider.wallet.publicKey));
});
@ -145,7 +145,7 @@ describe("misc", () => {
);
await assert.rejects(
async () => {
await program.account.testData(associatedAccount);
await program.account.testData.fetch(associatedAccount);
},
(err) => {
assert.ok(
@ -234,7 +234,7 @@ describe("misc", () => {
instructions: [await program.account.dataI8.createInstruction(data)],
signers: [data],
});
const dataAccount = await program.account.dataI8(data.publicKey);
const dataAccount = await program.account.dataI8.fetch(data.publicKey);
assert.ok(dataAccount.data === -3);
});
@ -250,14 +250,14 @@ describe("misc", () => {
instructions: [await program.account.dataI16.createInstruction(data)],
signers: [data],
});
const dataAccount = await program.account.dataI16(data.publicKey);
const dataAccount = await program.account.dataI16.fetch(data.publicKey);
assert.ok(dataAccount.data === -2048);
dataPubkey = data.publicKey;
});
it("Can use base58 strings to fetch an account", async () => {
const dataAccount = await program.account.dataI16(dataPubkey.toString());
const dataAccount = await program.account.dataI16.fetch(dataPubkey.toString());
assert.ok(dataAccount.data === -2048);
});
});

View File

@ -39,7 +39,7 @@ describe("multisig", () => {
signers: [multisig],
});
let multisigAccount = await program.account.multisig(multisig.publicKey);
let multisigAccount = await program.account.multisig.fetch(multisig.publicKey);
assert.equal(multisigAccount.nonce, nonce);
assert.ok(multisigAccount.threshold.eq(new anchor.BN(2)));
@ -81,7 +81,7 @@ describe("multisig", () => {
signers: [transaction, ownerA],
});
const txAccount = await program.account.transaction(transaction.publicKey);
const txAccount = await program.account.transaction.fetch(transaction.publicKey);
assert.ok(txAccount.programId.equals(pid));
assert.deepEqual(txAccount.accounts, accounts);
@ -124,7 +124,7 @@ describe("multisig", () => {
}),
});
multisigAccount = await program.account.multisig(multisig.publicKey);
multisigAccount = await program.account.multisig.fetch(multisig.publicKey);
assert.equal(multisigAccount.nonce, nonce);
assert.ok(multisigAccount.threshold.eq(new anchor.BN(2)));

View File

@ -43,7 +43,7 @@ describe("basic-1", () => {
// #endregion code-separated
// Fetch the newly created account from the cluster.
const account = await program.account.myAccount(myAccount.publicKey);
const account = await program.account.myAccount.fetch(myAccount.publicKey);
// Check it's state was initialized.
assert.ok(account.data.eq(new anchor.BN(1234)));
@ -81,7 +81,7 @@ describe("basic-1", () => {
});
// Fetch the newly created account from the cluster.
const account = await program.account.myAccount(myAccount.publicKey);
const account = await program.account.myAccount.fetch(myAccount.publicKey);
// Check it's state was initialized.
assert.ok(account.data.eq(new anchor.BN(1234)));
@ -108,7 +108,7 @@ describe("basic-1", () => {
// #endregion code-simplified
// Fetch the newly created account from the cluster.
const account = await program.account.myAccount(myAccount.publicKey);
const account = await program.account.myAccount.fetch(myAccount.publicKey);
// Check it's state was initialized.
assert.ok(account.data.eq(new anchor.BN(1234)));
@ -133,7 +133,7 @@ describe("basic-1", () => {
});
// Fetch the newly updated account.
const account = await program.account.myAccount(myAccount.publicKey);
const account = await program.account.myAccount.fetch(myAccount.publicKey);
// Check it's state was mutated.
assert.ok(account.data.eq(new anchor.BN(4321)));

View File

@ -23,7 +23,7 @@ describe('basic-2', () => {
instructions: [await program.account.counter.createInstruction(counter)],
})
let counterAccount = await program.account.counter(counter.publicKey)
let counterAccount = await program.account.counter.fetch(counter.publicKey)
assert.ok(counterAccount.authority.equals(provider.wallet.publicKey))
assert.ok(counterAccount.count.toNumber() === 0)
@ -37,7 +37,7 @@ describe('basic-2', () => {
},
})
const counterAccount = await program.account.counter(counter.publicKey)
const counterAccount = await program.account.counter.fetch(counter.publicKey)
assert.ok(counterAccount.authority.equals(provider.wallet.publicKey))
assert.ok(counterAccount.count.toNumber() == 1)

View File

@ -41,7 +41,7 @@ describe("basic-3", () => {
});
// Check the state updated.
puppetAccount = await puppet.account.puppet(newPuppetAccount.publicKey);
puppetAccount = await puppet.account.puppet.fetch(newPuppetAccount.publicKey);
assert.ok(puppetAccount.data.eq(new anchor.BN(111)));
});
});

View File

@ -21,7 +21,7 @@ describe("basic-4", () => {
// Fetch the state struct from the network.
// #region accessor
const state = await program.state();
const state = await program.state.fetch();
// #endregion accessor
assert.ok(state.count.eq(new anchor.BN(0)));
@ -35,7 +35,7 @@ describe("basic-4", () => {
},
});
// #endregion instruction
const state = await program.state();
const state = await program.state.fetch();
assert.ok(state.count.eq(new anchor.BN(1)));
});
});

View File

@ -17,7 +17,7 @@ describe("zero-copy", () => {
authority: program.provider.wallet.publicKey,
},
});
const state = await program.state();
const state = await program.state.fetch();
assert.ok(state.authority.equals(program.provider.wallet.publicKey));
assert.ok(state.events.length === 250);
state.events.forEach((event, idx) => {
@ -36,7 +36,7 @@ describe("zero-copy", () => {
authority: program.provider.wallet.publicKey,
},
});
const state = await program.state();
const state = await program.state.fetch();
assert.ok(state.authority.equals(program.provider.wallet.publicKey));
assert.ok(state.events.length === 250);
state.events.forEach((event, idx) => {
@ -60,7 +60,7 @@ describe("zero-copy", () => {
instructions: [await program.account.foo.createInstruction(foo)],
signers: [foo],
});
const account = await program.account.foo(foo.publicKey);
const account = await program.account.foo.fetch(foo.publicKey);
assert.ok(
JSON.stringify(account.authority.toBuffer()) ===
JSON.stringify(program.provider.wallet.publicKey.toBuffer())
@ -81,7 +81,7 @@ describe("zero-copy", () => {
},
});
const account = await program.account.foo(foo.publicKey);
const account = await program.account.foo.fetch(foo.publicKey);
assert.ok(
JSON.stringify(account.authority.toBuffer()) ===
@ -103,7 +103,7 @@ describe("zero-copy", () => {
},
});
const account = await program.account.foo(foo.publicKey);
const account = await program.account.foo.fetch(foo.publicKey);
assert.ok(
JSON.stringify(account.authority.toBuffer()) ===
@ -172,7 +172,7 @@ describe("zero-copy", () => {
],
signers: [eventQ],
});
const account = await program.account.eventQ(eventQ.publicKey);
const account = await program.account.eventQ.fetch(eventQ.publicKey);
assert.ok(account.events.length === 25000);
account.events.forEach((event) => {
assert.ok(event.from.equals(new PublicKey()));
@ -189,7 +189,7 @@ describe("zero-copy", () => {
},
});
// Verify update.
let account = await program.account.eventQ(eventQ.publicKey);
let account = await program.account.eventQ.fetch(eventQ.publicKey);
assert.ok(account.events.length === 25000);
account.events.forEach((event, idx) => {
if (idx === 0) {
@ -209,7 +209,7 @@ describe("zero-copy", () => {
},
});
// Verify update.
account = await program.account.eventQ(eventQ.publicKey);
account = await program.account.eventQ.fetch(eventQ.publicKey);
assert.ok(account.events.length === 25000);
account.events.forEach((event, idx) => {
if (idx === 0) {
@ -232,7 +232,7 @@ describe("zero-copy", () => {
},
});
// Verify update.
account = await program.account.eventQ(eventQ.publicKey);
account = await program.account.eventQ.fetch(eventQ.publicKey);
assert.ok(account.events.length === 25000);
account.events.forEach((event, idx) => {
if (idx === 0) {

View File

@ -18,7 +18,7 @@
"lint:fix": "prettier src/** -w",
"watch": "tsc -p tsconfig.cjs.json --watch",
"prepublishOnly": "yarn build",
"docs": "typedoc --excludePrivate --includeVersion --out ../docs/src/.vuepress/dist/ts/ src/index.ts"
"docs": "typedoc --excludePrivate --includeVersion --out ../docs/src/.vuepress/dist/ts/ --readme none src/index.ts"
},
"dependencies": {
"@project-serum/borsh": "^0.2.2",

View File

@ -1,571 +0,0 @@
import camelCase from "camelcase";
import * as base64 from "base64-js";
import { snakeCase } from "snake-case";
import { Layout } from "buffer-layout";
import * as sha256 from "js-sha256";
import * as borsh from "@project-serum/borsh";
import {
Idl,
IdlField,
IdlTypeDef,
IdlEnumVariant,
IdlType,
IdlStateMethod,
} from "./idl";
import { IdlError } from "./error";
import { Event } from "./program/event";
/**
* Number of bytes of the account discriminator.
*/
export const ACCOUNT_DISCRIMINATOR_SIZE = 8;
/**
* Namespace for state method function signatures.
*/
export const SIGHASH_STATE_NAMESPACE = "state";
/**
* Namespace for global instruction function signatures (i.e. functions
* that aren't namespaced by the state or any of its trait implementations).
*/
export const SIGHASH_GLOBAL_NAMESPACE = "global";
/**
* Coder provides a facade for encoding and decoding all IDL related objects.
*/
export default class Coder {
/**
* Instruction coder.
*/
readonly instruction: InstructionCoder;
/**
* Account coder.
*/
readonly accounts: AccountsCoder;
/**
* Types coder.
*/
readonly types: TypesCoder;
/**
* Coder for state structs.
*/
readonly state: StateCoder;
/**
* Coder for events.
*/
readonly events: EventCoder;
constructor(idl: Idl) {
this.instruction = new InstructionCoder(idl);
this.accounts = new AccountsCoder(idl);
this.types = new TypesCoder(idl);
this.events = new EventCoder(idl);
if (idl.state) {
this.state = new StateCoder(idl);
}
}
public sighash(nameSpace: string, ixName: string): Buffer {
return sighash(nameSpace, ixName);
}
}
/**
* Encodes and decodes program instructions.
*/
class InstructionCoder {
/**
* Instruction args layout. Maps namespaced method
*/
private ixLayout: Map<string, Layout>;
public constructor(idl: Idl) {
this.ixLayout = InstructionCoder.parseIxLayout(idl);
}
/**
* Encodes a program instruction.
*/
public encode(ixName: string, ix: any) {
return this._encode(SIGHASH_GLOBAL_NAMESPACE, ixName, ix);
}
/**
* Encodes a program state instruction.
*/
public encodeState(ixName: string, ix: any) {
return this._encode(SIGHASH_STATE_NAMESPACE, ixName, ix);
}
private _encode(nameSpace: string, ixName: string, ix: any): Buffer {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const methodName = camelCase(ixName);
const len = this.ixLayout.get(methodName).encode(ix, buffer);
const data = buffer.slice(0, len);
return Buffer.concat([sighash(nameSpace, ixName), data]);
}
private static parseIxLayout(idl: Idl): Map<string, Layout> {
const stateMethods = idl.state ? idl.state.methods : [];
const ixLayouts = stateMethods
.map((m: IdlStateMethod) => {
let fieldLayouts = m.args.map((arg: IdlField) => {
return IdlCoder.fieldLayout(arg, idl.types);
});
const name = camelCase(m.name);
return [name, borsh.struct(fieldLayouts, name)];
})
.concat(
idl.instructions.map((ix) => {
let fieldLayouts = ix.args.map((arg: IdlField) =>
IdlCoder.fieldLayout(arg, idl.types)
);
const name = camelCase(ix.name);
return [name, borsh.struct(fieldLayouts, name)];
})
);
// @ts-ignore
return new Map(ixLayouts);
}
}
/**
* Encodes and decodes account objects.
*/
class AccountsCoder {
/**
* Maps account type identifier to a layout.
*/
private accountLayouts: Map<string, Layout>;
public constructor(idl: Idl) {
if (idl.accounts === undefined) {
this.accountLayouts = new Map();
return;
}
const layouts: [string, Layout][] = idl.accounts.map((acc) => {
return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
});
this.accountLayouts = new Map(layouts);
}
public async encode<T = any>(
accountName: string,
account: T
): Promise<Buffer> {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const layout = this.accountLayouts.get(accountName);
const len = layout.encode(account, buffer);
let accountData = buffer.slice(0, len);
let discriminator = await accountDiscriminator(accountName);
return Buffer.concat([discriminator, accountData]);
}
public decode<T = any>(accountName: string, ix: Buffer): T {
// Chop off the discriminator before decoding.
const data = ix.slice(8);
const layout = this.accountLayouts.get(accountName);
return layout.decode(data);
}
}
/**
* Encodes and decodes user defined types.
*/
class TypesCoder {
/**
* Maps account type identifier to a layout.
*/
private layouts: Map<string, Layout>;
public constructor(idl: Idl) {
if (idl.types === undefined) {
this.layouts = new Map();
return;
}
const layouts = idl.types.map((acc) => {
return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
});
// @ts-ignore
this.layouts = new Map(layouts);
}
public encode<T = any>(accountName: string, account: T): Buffer {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const layout = this.layouts.get(accountName);
const len = layout.encode(account, buffer);
return buffer.slice(0, len);
}
public decode<T = any>(accountName: string, ix: Buffer): T {
const layout = this.layouts.get(accountName);
return layout.decode(ix);
}
}
class EventCoder {
/**
* Maps account type identifier to a layout.
*/
private layouts: Map<string, Layout>;
/**
* Maps base64 encoded event discriminator to event name.
*/
private discriminators: Map<string, string>;
public constructor(idl: Idl) {
if (idl.events === undefined) {
this.layouts = new Map();
return;
}
const layouts = idl.events.map((event) => {
let eventTypeDef: IdlTypeDef = {
name: event.name,
type: {
kind: "struct",
fields: event.fields.map((f) => {
return { name: f.name, type: f.type };
}),
},
};
return [event.name, IdlCoder.typeDefLayout(eventTypeDef, idl.types)];
});
// @ts-ignore
this.layouts = new Map(layouts);
this.discriminators = new Map<string, string>(
idl.events === undefined
? []
: idl.events.map((e) => [
base64.fromByteArray(eventDiscriminator(e.name)),
e.name,
])
);
}
public decode(log: string): Event | null {
const logArr = Buffer.from(base64.toByteArray(log));
const disc = base64.fromByteArray(logArr.slice(0, 8));
// Only deserialize if the discriminator implies a proper event.
const eventName = this.discriminators.get(disc);
if (eventName === undefined) {
return null;
}
const layout = this.layouts.get(eventName);
const data = layout.decode(logArr.slice(8));
return { data, name: eventName };
}
}
class StateCoder {
private layout: Layout;
public constructor(idl: Idl) {
if (idl.state === undefined) {
throw new Error("Idl state not defined.");
}
this.layout = IdlCoder.typeDefLayout(idl.state.struct, idl.types);
}
public async encode<T = any>(name: string, account: T): Promise<Buffer> {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const len = this.layout.encode(account, buffer);
const disc = await stateDiscriminator(name);
const accData = buffer.slice(0, len);
return Buffer.concat([disc, accData]);
}
public decode<T = any>(ix: Buffer): T {
// Chop off discriminator.
const data = ix.slice(8);
return this.layout.decode(data);
}
}
class IdlCoder {
public static fieldLayout(field: IdlField, types?: IdlTypeDef[]): Layout {
const fieldName =
field.name !== undefined ? camelCase(field.name) : undefined;
switch (field.type) {
case "bool": {
return borsh.bool(fieldName);
}
case "u8": {
return borsh.u8(fieldName);
}
case "i8": {
return borsh.i8(fieldName);
}
case "u16": {
return borsh.u16(fieldName);
}
case "i16": {
return borsh.i16(fieldName);
}
case "u32": {
return borsh.u32(fieldName);
}
case "i32": {
return borsh.i32(fieldName);
}
case "u64": {
return borsh.u64(fieldName);
}
case "i64": {
return borsh.i64(fieldName);
}
case "u128": {
return borsh.u128(fieldName);
}
case "i128": {
return borsh.i128(fieldName);
}
case "bytes": {
return borsh.vecU8(fieldName);
}
case "string": {
return borsh.str(fieldName);
}
case "publicKey": {
return borsh.publicKey(fieldName);
}
default: {
// @ts-ignore
if (field.type.vec) {
return borsh.vec(
IdlCoder.fieldLayout(
{
name: undefined,
// @ts-ignore
type: field.type.vec,
},
types
),
fieldName
);
// @ts-ignore
} else if (field.type.option) {
return borsh.option(
IdlCoder.fieldLayout(
{
name: undefined,
// @ts-ignore
type: field.type.option,
},
types
),
fieldName
);
// @ts-ignore
} else if (field.type.defined) {
// User defined type.
if (types === undefined) {
throw new IdlError("User defined types not provided");
}
// @ts-ignore
const filtered = types.filter((t) => t.name === field.type.defined);
if (filtered.length !== 1) {
throw new IdlError(`Type not found: ${JSON.stringify(field)}`);
}
return IdlCoder.typeDefLayout(filtered[0], types, fieldName);
// @ts-ignore
} else if (field.type.array) {
// @ts-ignore
let arrayTy = field.type.array[0];
// @ts-ignore
let arrayLen = field.type.array[1];
let innerLayout = IdlCoder.fieldLayout(
{
name: undefined,
type: arrayTy,
},
types
);
return borsh.array(innerLayout, arrayLen, fieldName);
} else {
throw new Error(`Not yet implemented: ${field}`);
}
}
}
}
public static typeDefLayout(
typeDef: IdlTypeDef,
types: IdlTypeDef[],
name?: string
): Layout {
if (typeDef.type.kind === "struct") {
const fieldLayouts = typeDef.type.fields.map((field) => {
const x = IdlCoder.fieldLayout(field, types);
return x;
});
return borsh.struct(fieldLayouts, name);
} else if (typeDef.type.kind === "enum") {
let variants = typeDef.type.variants.map((variant: IdlEnumVariant) => {
const name = camelCase(variant.name);
if (variant.fields === undefined) {
return borsh.struct([], name);
}
// @ts-ignore
const fieldLayouts = variant.fields.map((f: IdlField | IdlType) => {
// @ts-ignore
if (f.name === undefined) {
throw new Error("Tuple enum variants not yet implemented.");
}
// @ts-ignore
return IdlCoder.fieldLayout(f, types);
});
return borsh.struct(fieldLayouts, name);
});
if (name !== undefined) {
// Buffer-layout lib requires the name to be null (on construction)
// when used as a field.
return borsh.rustEnum(variants).replicate(name);
}
return borsh.rustEnum(variants, name);
} else {
throw new Error(`Unknown type kint: ${typeDef}`);
}
}
}
// Calculates unique 8 byte discriminator prepended to all anchor accounts.
export async function accountDiscriminator(name: string): Promise<Buffer> {
// @ts-ignore
return Buffer.from(sha256.digest(`account:${name}`)).slice(0, 8);
}
// Calculates unique 8 byte discriminator prepended to all anchor state accounts.
export async function stateDiscriminator(name: string): Promise<Buffer> {
// @ts-ignore
return Buffer.from(sha256.digest(`state:${name}`)).slice(0, 8);
}
export function eventDiscriminator(name: string): Buffer {
// @ts-ignore
return Buffer.from(sha256.digest(`event:${name}`)).slice(0, 8);
}
// Returns the size of the type in bytes. For variable length types, just return
// 1. Users should override this value in such cases.
function typeSize(idl: Idl, ty: IdlType): number {
switch (ty) {
case "bool":
return 1;
case "u8":
return 1;
case "i8":
return 1;
case "i16":
return 2;
case "u16":
return 2;
case "u32":
return 4;
case "i32":
return 4;
case "u64":
return 8;
case "i64":
return 8;
case "u128":
return 16;
case "i128":
return 16;
case "bytes":
return 1;
case "string":
return 1;
case "publicKey":
return 32;
default:
// @ts-ignore
if (ty.vec !== undefined) {
return 1;
}
// @ts-ignore
if (ty.option !== undefined) {
// @ts-ignore
return 1 + typeSize(idl, ty.option);
}
// @ts-ignore
if (ty.defined !== undefined) {
// @ts-ignore
const filtered = idl.types.filter((t) => t.name === ty.defined);
if (filtered.length !== 1) {
throw new IdlError(`Type not found: ${JSON.stringify(ty)}`);
}
let typeDef = filtered[0];
return accountSize(idl, typeDef);
}
// @ts-ignore
if (ty.array !== undefined) {
// @ts-ignore
let arrayTy = ty.array[0];
// @ts-ignore
let arraySize = ty.array[1];
// @ts-ignore
return typeSize(idl, arrayTy) * arraySize;
}
throw new Error(`Invalid type ${JSON.stringify(ty)}`);
}
}
export function accountSize(
idl: Idl,
idlAccount: IdlTypeDef
): number | undefined {
if (idlAccount.type.kind === "enum") {
let variantSizes = idlAccount.type.variants.map(
(variant: IdlEnumVariant) => {
if (variant.fields === undefined) {
return 0;
}
// @ts-ignore
return (
variant.fields
// @ts-ignore
.map((f: IdlField | IdlType) => {
// @ts-ignore
if (f.name === undefined) {
throw new Error("Tuple enum variants not yet implemented.");
}
// @ts-ignore
return typeSize(idl, f.type);
})
.reduce((a: number, b: number) => a + b)
);
}
);
return Math.max(...variantSizes) + 1;
}
if (idlAccount.type.fields === undefined) {
return 0;
}
return idlAccount.type.fields
.map((f) => typeSize(idl, f.type))
.reduce((a, b) => a + b);
}
// Not technically sighash, since we don't include the arguments, as Rust
// doesn't allow function overloading.
function sighash(nameSpace: string, ixName: string): Buffer {
let name = snakeCase(ixName);
let preimage = `${nameSpace}:${name}`;
// @ts-ignore
return Buffer.from(sha256.digest(preimage)).slice(0, 8);
}

55
ts/src/coder/accounts.ts Normal file
View File

@ -0,0 +1,55 @@
import { Layout } from "buffer-layout";
import { Idl } from "../idl";
import { IdlCoder } from "./idl";
import { sha256 } from "js-sha256";
/**
* Number of bytes of the account discriminator.
*/
export const ACCOUNT_DISCRIMINATOR_SIZE = 8;
/**
* Encodes and decodes account objects.
*/
export class AccountsCoder {
/**
* Maps account type identifier to a layout.
*/
private accountLayouts: Map<string, Layout>;
public constructor(idl: Idl) {
if (idl.accounts === undefined) {
this.accountLayouts = new Map();
return;
}
const layouts: [string, Layout][] = idl.accounts.map((acc) => {
return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
});
this.accountLayouts = new Map(layouts);
}
public async encode<T = any>(
accountName: string,
account: T
): Promise<Buffer> {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const layout = this.accountLayouts.get(accountName);
const len = layout.encode(account, buffer);
let accountData = buffer.slice(0, len);
let discriminator = await accountDiscriminator(accountName);
return Buffer.concat([discriminator, accountData]);
}
public decode<T = any>(accountName: string, ix: Buffer): T {
// Chop off the discriminator before decoding.
const data = ix.slice(8);
const layout = this.accountLayouts.get(accountName);
return layout.decode(data);
}
}
// Calculates unique 8 byte discriminator prepended to all anchor accounts.
export async function accountDiscriminator(name: string): Promise<Buffer> {
return Buffer.from(sha256.digest(`account:${name}`)).slice(0, 8);
}

113
ts/src/coder/common.ts Normal file
View File

@ -0,0 +1,113 @@
import { snakeCase } from "snake-case";
import { sha256 } from "js-sha256";
import { Idl, IdlField, IdlTypeDef, IdlEnumVariant, IdlType } from "../idl";
import { IdlError } from "../error";
export function accountSize(
idl: Idl,
idlAccount: IdlTypeDef
): number | undefined {
if (idlAccount.type.kind === "enum") {
let variantSizes = idlAccount.type.variants.map(
(variant: IdlEnumVariant) => {
if (variant.fields === undefined) {
return 0;
}
return (
variant.fields
// @ts-ignore
.map((f: IdlField | IdlType) => {
// @ts-ignore
if (f.name === undefined) {
throw new Error("Tuple enum variants not yet implemented.");
}
// @ts-ignore
return typeSize(idl, f.type);
})
.reduce((a: number, b: number) => a + b)
);
}
);
return Math.max(...variantSizes) + 1;
}
if (idlAccount.type.fields === undefined) {
return 0;
}
return idlAccount.type.fields
.map((f) => typeSize(idl, f.type))
.reduce((a, b) => a + b);
}
// Returns the size of the type in bytes. For variable length types, just return
// 1. Users should override this value in such cases.
function typeSize(idl: Idl, ty: IdlType): number {
switch (ty) {
case "bool":
return 1;
case "u8":
return 1;
case "i8":
return 1;
case "i16":
return 2;
case "u16":
return 2;
case "u32":
return 4;
case "i32":
return 4;
case "u64":
return 8;
case "i64":
return 8;
case "u128":
return 16;
case "i128":
return 16;
case "bytes":
return 1;
case "string":
return 1;
case "publicKey":
return 32;
default:
// @ts-ignore
if (ty.vec !== undefined) {
return 1;
}
// @ts-ignore
if (ty.option !== undefined) {
// @ts-ignore
return 1 + typeSize(idl, ty.option);
}
// @ts-ignore
if (ty.defined !== undefined) {
// @ts-ignore
const filtered = idl.types.filter((t) => t.name === ty.defined);
if (filtered.length !== 1) {
throw new IdlError(`Type not found: ${JSON.stringify(ty)}`);
}
let typeDef = filtered[0];
return accountSize(idl, typeDef);
}
// @ts-ignore
if (ty.array !== undefined) {
// @ts-ignore
let arrayTy = ty.array[0];
// @ts-ignore
let arraySize = ty.array[1];
// @ts-ignore
return typeSize(idl, arrayTy) * arraySize;
}
throw new Error(`Invalid type ${JSON.stringify(ty)}`);
}
}
// Not technically sighash, since we don't include the arguments, as Rust
// doesn't allow function overloading.
export function sighash(nameSpace: string, ixName: string): Buffer {
let name = snakeCase(ixName);
let preimage = `${nameSpace}:${name}`;
return Buffer.from(sha256.digest(preimage)).slice(0, 8);
}

67
ts/src/coder/event.ts Normal file
View File

@ -0,0 +1,67 @@
import * as base64 from "base64-js";
import { Layout } from "buffer-layout";
import { sha256 } from "js-sha256";
import { Idl, IdlTypeDef } from "../idl";
import { Event } from "../program/event";
import { IdlCoder } from "./idl";
export class EventCoder {
/**
* Maps account type identifier to a layout.
*/
private layouts: Map<string, Layout>;
/**
* Maps base64 encoded event discriminator to event name.
*/
private discriminators: Map<string, string>;
public constructor(idl: Idl) {
if (idl.events === undefined) {
this.layouts = new Map();
return;
}
const layouts = idl.events.map((event) => {
let eventTypeDef: IdlTypeDef = {
name: event.name,
type: {
kind: "struct",
fields: event.fields.map((f) => {
return { name: f.name, type: f.type };
}),
},
};
return [event.name, IdlCoder.typeDefLayout(eventTypeDef, idl.types)];
});
// @ts-ignore
this.layouts = new Map(layouts);
this.discriminators = new Map<string, string>(
idl.events === undefined
? []
: idl.events.map((e) => [
base64.fromByteArray(eventDiscriminator(e.name)),
e.name,
])
);
}
public decode(log: string): Event | null {
const logArr = Buffer.from(base64.toByteArray(log));
const disc = base64.fromByteArray(logArr.slice(0, 8));
// Only deserialize if the discriminator implies a proper event.
const eventName = this.discriminators.get(disc);
if (eventName === undefined) {
return null;
}
const layout = this.layouts.get(eventName);
const data = layout.decode(logArr.slice(8));
return { data, name: eventName };
}
}
export function eventDiscriminator(name: string): Buffer {
return Buffer.from(sha256.digest(`event:${name}`)).slice(0, 8);
}

154
ts/src/coder/idl.ts Normal file
View File

@ -0,0 +1,154 @@
import camelCase from "camelcase";
import { Layout } from "buffer-layout";
import * as borsh from "@project-serum/borsh";
import { IdlField, IdlTypeDef, IdlEnumVariant, IdlType } from "../idl";
import { IdlError } from "../error";
export class IdlCoder {
public static fieldLayout(field: IdlField, types?: IdlTypeDef[]): Layout {
const fieldName =
field.name !== undefined ? camelCase(field.name) : undefined;
switch (field.type) {
case "bool": {
return borsh.bool(fieldName);
}
case "u8": {
return borsh.u8(fieldName);
}
case "i8": {
return borsh.i8(fieldName);
}
case "u16": {
return borsh.u16(fieldName);
}
case "i16": {
return borsh.i16(fieldName);
}
case "u32": {
return borsh.u32(fieldName);
}
case "i32": {
return borsh.i32(fieldName);
}
case "u64": {
return borsh.u64(fieldName);
}
case "i64": {
return borsh.i64(fieldName);
}
case "u128": {
return borsh.u128(fieldName);
}
case "i128": {
return borsh.i128(fieldName);
}
case "bytes": {
return borsh.vecU8(fieldName);
}
case "string": {
return borsh.str(fieldName);
}
case "publicKey": {
return borsh.publicKey(fieldName);
}
default: {
// @ts-ignore
if (field.type.vec) {
return borsh.vec(
IdlCoder.fieldLayout(
{
name: undefined,
// @ts-ignore
type: field.type.vec,
},
types
),
fieldName
);
// @ts-ignore
} else if (field.type.option) {
return borsh.option(
IdlCoder.fieldLayout(
{
name: undefined,
// @ts-ignore
type: field.type.option,
},
types
),
fieldName
);
// @ts-ignore
} else if (field.type.defined) {
// User defined type.
if (types === undefined) {
throw new IdlError("User defined types not provided");
}
// @ts-ignore
const filtered = types.filter((t) => t.name === field.type.defined);
if (filtered.length !== 1) {
throw new IdlError(`Type not found: ${JSON.stringify(field)}`);
}
return IdlCoder.typeDefLayout(filtered[0], types, fieldName);
// @ts-ignore
} else if (field.type.array) {
// @ts-ignore
let arrayTy = field.type.array[0];
// @ts-ignore
let arrayLen = field.type.array[1];
let innerLayout = IdlCoder.fieldLayout(
{
name: undefined,
type: arrayTy,
},
types
);
return borsh.array(innerLayout, arrayLen, fieldName);
} else {
throw new Error(`Not yet implemented: ${field}`);
}
}
}
}
public static typeDefLayout(
typeDef: IdlTypeDef,
types: IdlTypeDef[],
name?: string
): Layout {
if (typeDef.type.kind === "struct") {
const fieldLayouts = typeDef.type.fields.map((field) => {
const x = IdlCoder.fieldLayout(field, types);
return x;
});
return borsh.struct(fieldLayouts, name);
} else if (typeDef.type.kind === "enum") {
let variants = typeDef.type.variants.map((variant: IdlEnumVariant) => {
const name = camelCase(variant.name);
if (variant.fields === undefined) {
return borsh.struct([], name);
}
// @ts-ignore
const fieldLayouts = variant.fields.map((f: IdlField | IdlType) => {
// @ts-ignore
if (f.name === undefined) {
throw new Error("Tuple enum variants not yet implemented.");
}
// @ts-ignore
return IdlCoder.fieldLayout(f, types);
});
return borsh.struct(fieldLayouts, name);
});
if (name !== undefined) {
// Buffer-layout lib requires the name to be null (on construction)
// when used as a field.
return borsh.rustEnum(variants).replicate(name);
}
return borsh.rustEnum(variants, name);
} else {
throw new Error(`Unknown type kint: ${typeDef}`);
}
}
}

62
ts/src/coder/index.ts Normal file
View File

@ -0,0 +1,62 @@
import { Idl } from "../idl";
import { InstructionCoder } from "./instruction";
import { AccountsCoder } from "./accounts";
import { TypesCoder } from "./types";
import { EventCoder } from "./event";
import { StateCoder } from "./state";
import { sighash } from "./common";
export { accountSize } from "./common";
export { TypesCoder } from "./types";
export { InstructionCoder } from "./instruction";
export {
AccountsCoder,
accountDiscriminator,
ACCOUNT_DISCRIMINATOR_SIZE,
} from "./accounts";
export { EventCoder, eventDiscriminator } from "./event";
export { StateCoder, stateDiscriminator } from "./state";
/**
* Coder provides a facade for encoding and decoding all IDL related objects.
*/
export default class Coder {
/**
* Instruction coder.
*/
readonly instruction: InstructionCoder;
/**
* Account coder.
*/
readonly accounts: AccountsCoder;
/**
* Types coder.
*/
readonly types: TypesCoder;
/**
* Coder for state structs.
*/
readonly state: StateCoder;
/**
* Coder for events.
*/
readonly events: EventCoder;
constructor(idl: Idl) {
this.instruction = new InstructionCoder(idl);
this.accounts = new AccountsCoder(idl);
this.types = new TypesCoder(idl);
this.events = new EventCoder(idl);
if (idl.state) {
this.state = new StateCoder(idl);
}
}
public sighash(nameSpace: string, ixName: string): Buffer {
return sighash(nameSpace, ixName);
}
}

View File

@ -0,0 +1,76 @@
import camelCase from "camelcase";
import { Layout } from "buffer-layout";
import * as borsh from "@project-serum/borsh";
import { Idl, IdlField, IdlStateMethod } from "../idl";
import { IdlCoder } from "./idl";
import { sighash } from "./common";
/**
* Namespace for state method function signatures.
*/
export const SIGHASH_STATE_NAMESPACE = "state";
/**
* Namespace for global instruction function signatures (i.e. functions
* that aren't namespaced by the state or any of its trait implementations).
*/
export const SIGHASH_GLOBAL_NAMESPACE = "global";
/**
* Encodes and decodes program instructions.
*/
export class InstructionCoder {
/**
* Instruction args layout. Maps namespaced method
*/
private ixLayout: Map<string, Layout>;
public constructor(idl: Idl) {
this.ixLayout = InstructionCoder.parseIxLayout(idl);
}
/**
* Encodes a program instruction.
*/
public encode(ixName: string, ix: any) {
return this._encode(SIGHASH_GLOBAL_NAMESPACE, ixName, ix);
}
/**
* Encodes a program state instruction.
*/
public encodeState(ixName: string, ix: any) {
return this._encode(SIGHASH_STATE_NAMESPACE, ixName, ix);
}
private _encode(nameSpace: string, ixName: string, ix: any): Buffer {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const methodName = camelCase(ixName);
const len = this.ixLayout.get(methodName).encode(ix, buffer);
const data = buffer.slice(0, len);
return Buffer.concat([sighash(nameSpace, ixName), data]);
}
private static parseIxLayout(idl: Idl): Map<string, Layout> {
const stateMethods = idl.state ? idl.state.methods : [];
const ixLayouts = stateMethods
.map((m: IdlStateMethod) => {
let fieldLayouts = m.args.map((arg: IdlField) => {
return IdlCoder.fieldLayout(arg, idl.types);
});
const name = camelCase(m.name);
return [name, borsh.struct(fieldLayouts, name)];
})
.concat(
idl.instructions.map((ix) => {
let fieldLayouts = ix.args.map((arg: IdlField) =>
IdlCoder.fieldLayout(arg, idl.types)
);
const name = camelCase(ix.name);
return [name, borsh.struct(fieldLayouts, name)];
})
);
// @ts-ignore
return new Map(ixLayouts);
}
}

36
ts/src/coder/state.ts Normal file
View File

@ -0,0 +1,36 @@
import { Layout } from "buffer-layout";
import { sha256 } from "js-sha256";
import { Idl } from "../idl";
import { IdlCoder } from "./idl";
export class StateCoder {
private layout: Layout;
public constructor(idl: Idl) {
if (idl.state === undefined) {
throw new Error("Idl state not defined.");
}
this.layout = IdlCoder.typeDefLayout(idl.state.struct, idl.types);
}
public async encode<T = any>(name: string, account: T): Promise<Buffer> {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const len = this.layout.encode(account, buffer);
const disc = await stateDiscriminator(name);
const accData = buffer.slice(0, len);
return Buffer.concat([disc, accData]);
}
public decode<T = any>(ix: Buffer): T {
// Chop off discriminator.
const data = ix.slice(8);
return this.layout.decode(data);
}
}
// Calculates unique 8 byte discriminator prepended to all anchor state accounts.
export async function stateDiscriminator(name: string): Promise<Buffer> {
return Buffer.from(sha256.digest(`state:${name}`)).slice(0, 8);
}

38
ts/src/coder/types.ts Normal file
View File

@ -0,0 +1,38 @@
import { Layout } from "buffer-layout";
import { Idl } from "../idl";
import { IdlCoder } from "./idl";
/**
* Encodes and decodes user defined types.
*/
export class TypesCoder {
/**
* Maps account type identifier to a layout.
*/
private layouts: Map<string, Layout>;
public constructor(idl: Idl) {
if (idl.types === undefined) {
this.layouts = new Map();
return;
}
const layouts = idl.types.map((acc) => {
return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
});
// @ts-ignore
this.layouts = new Map(layouts);
}
public encode<T = any>(accountName: string, account: T): Buffer {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const layout = this.layouts.get(accountName);
const len = layout.encode(account, buffer);
return buffer.slice(0, len);
}
public decode<T = any>(accountName: string, ix: Buffer): T {
const layout = this.layouts.get(accountName);
return layout.decode(ix);
}
}

View File

@ -29,8 +29,6 @@ export type IdlInstruction = {
args: IdlField[];
};
// IdlStateMethods are similar to instructions, except they only allow
// for a single account, the state account.
export type IdlState = {
struct: IdlTypeDef;
methods: IdlStateMethod[];
@ -80,6 +78,8 @@ export type IdlType =
| "i32"
| "u64"
| "i64"
| "u128"
| "i128"
| "bytes"
| "string"
| "publicKey"

View File

@ -1,21 +1,46 @@
import BN from "bn.js";
import * as web3 from "@solana/web3.js";
import Provider, { NodeWallet as Wallet } from "./provider";
import Coder from "./coder";
import Coder, {
InstructionCoder,
EventCoder,
StateCoder,
TypesCoder,
} from "./coder";
import { Idl } from "./idl";
import workspace from "./workspace";
import utils from "./utils";
import { Program } from "./program";
import { Address } from "./program/common";
import { ProgramAccount } from "./program/namespace";
import { Event } from "./program/event";
import {
ProgramAccount,
AccountNamespace,
AccountClient,
StateClient,
RpcNamespace,
RpcFn,
SimulateNamespace,
SimulateFn,
TransactionNamespace,
TransactionFn,
InstructionNamespace,
InstructionFn,
} from "./program/namespace";
import { Context, Accounts } from "./program/context";
let _provider: Provider | null = null;
/**
* Sets the default provider on the client.
*/
function setProvider(provider: Provider) {
_provider = provider;
}
/**
* Returns the default provider being used by the client.
*/
function getProvider(): Provider {
if (_provider === null) {
return Provider.local();
@ -26,10 +51,26 @@ function getProvider(): Provider {
export {
workspace,
Program,
AccountNamespace,
AccountClient,
StateClient,
RpcNamespace,
RpcFn,
SimulateNamespace,
SimulateFn,
TransactionNamespace,
TransactionFn,
InstructionNamespace,
InstructionFn,
ProgramAccount,
Context,
Accounts,
Coder,
InstructionCoder,
EventCoder,
StateCoder,
TypesCoder,
Event,
setProvider,
getProvider,
Provider,

View File

@ -1,10 +1,8 @@
import EventEmitter from "eventemitter3";
import * as bs58 from "bs58";
import { PublicKey } from "@solana/web3.js";
import { Idl, IdlInstruction, IdlAccountItem, IdlStateMethod } from "../idl";
import { ProgramError } from "../error";
import { Accounts } from "./context";
import Provider from "../provider";
export type Subscription = {
listener: number;

View File

@ -8,7 +8,7 @@ import NamespaceFactory, {
InstructionNamespace,
TransactionNamespace,
AccountNamespace,
StateNamespace,
StateClient,
SimulateNamespace,
} from "./namespace";
import { getProvider } from "../";
@ -28,27 +28,29 @@ import { Address, translateAddress } from "./common";
* changes, and listen to events.
*
* In addition to field accessors and methods, the object provides a set of
* dynamically generated properties (internally referred to as namespaces) that
* map one-to-one to program instructions and accounts. These namespaces
* generally can be used as follows:
* dynamically generated properties, also known as namespaces, that
* map one-to-one to program methods and accounts. These namespaces generally
* can be used as follows:
*
* ## Usage
*
* ```javascript
* program.<namespace>.<program-specific-field>
* program.<namespace>.<program-specific-method>
* ```
*
* API specifics are namespace dependent. The examples used in the documentation
* below will refer to the two counter examples found
* [here](https://project-serum.github.io/anchor/ts/#examples).
* [here](https://github.com/project-serum/anchor#examples).
*/
export class Program {
/**
* Async methods to send signed transactions invoking *non*-state methods
* on an Anchor program.
* Async methods to send signed transactions to *non*-state methods on the
* program, returning a [[TransactionSignature]].
*
* ## rpc
* ## Usage
*
* ```javascript
* program.rpc.<method>(...args, ctx);
* rpc.<method>(...args, ctx);
* ```
*
* ## Parameters
@ -74,32 +76,32 @@ export class Program {
readonly rpc: RpcNamespace;
/**
* Async functions to fetch deserialized program accounts from a cluster.
* The namespace provides handles to an [[AccountClient]] object for each
* account in the program.
*
* ## account
* ## Usage
*
* ```javascript
* program.account.<account>(address);
* program.account.<account-client>
* ```
*
* ## Parameters
*
* 1. `address` - The [[Address]] of the account.
*
* ## Example
*
* To fetch a `Counter` object from the above example,
* To fetch a `Counter` account from the above example,
*
* ```javascript
* const counter = await program.account.counter(address);
* const counter = await program.account.counter.fetch(address);
* ```
*
* For the full API, see the [[AccountClient]] reference.
*/
readonly account: AccountNamespace;
/**
* Functions to build [[TransactionInstruction]] objects for program methods.
* The namespace provides functions to build [[TransactionInstruction]]
* objects for each method of a program.
*
* ## instruction
* ## Usage
*
* ```javascript
* program.instruction.<method>(...args, ctx);
@ -127,9 +129,10 @@ export class Program {
readonly instruction: InstructionNamespace;
/**
* Functions to build [[Transaction]] objects.
* The namespace provides functions to build [[Transaction]] objects for each
* method of a program.
*
* ## transaction
* ## Usage
*
* ```javascript
* program.transaction.<method>(...args, ctx);
@ -157,8 +160,9 @@ export class Program {
readonly transaction: TransactionNamespace;
/**
* Async functions to simulate instructions against an Anchor program,
* returning a list of deserialized events *and* raw program logs.
* The namespace provides functions to simulate transactions for each method
* of a program, returning a list of deserialized events *and* raw program
* logs.
*
* One can use this to read data calculated from a program on chain, by
* emitting an event in the program and reading the emitted event client side
@ -182,7 +186,7 @@ export class Program {
* To simulate the `increment` method above,
*
* ```javascript
* const tx = await program.simulate.increment({
* const events = await program.simulate.increment({
* accounts: {
* counter,
* },
@ -192,9 +196,11 @@ export class Program {
readonly simulate: SimulateNamespace;
/**
* Object with state account accessors and rpcs.
* 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: StateNamespace;
readonly state: StateClient;
/**
* Address of the program.
@ -249,15 +255,15 @@ export class Program {
instruction,
transaction,
account,
state,
simulate,
state,
] = NamespaceFactory.build(idl, this._coder, programId, this._provider);
this.rpc = rpc;
this.instruction = instruction;
this.transaction = transaction;
this.account = account;
this.state = state;
this.simulate = simulate;
this.state = state;
}
/**

View File

@ -9,39 +9,260 @@ import {
Commitment,
} from "@solana/web3.js";
import Provider from "../../provider";
import { Idl } from "../../idl";
import { Idl, IdlTypeDef } from "../../idl";
import Coder, {
ACCOUNT_DISCRIMINATOR_SIZE,
accountDiscriminator,
accountSize,
} from "../../coder";
import { Subscription, Address, translateAddress } from "../common";
import { getProvider } from "../../";
/**
* Accounts is a dynamically generated object to fetch any given account
* of a program.
*/
export interface AccountNamespace {
[key: string]: AccountFn;
export default class AccountFactory {
public static build(
idl: Idl,
coder: Coder,
programId: PublicKey,
provider: Provider
): AccountNamespace {
const accountFns: AccountNamespace = {};
idl.accounts.forEach((idlAccount) => {
const name = camelCase(idlAccount.name);
accountFns[name] = new AccountClient(
idl,
idlAccount,
programId,
provider,
coder
);
});
return accountFns;
}
}
/**
* Account is a function returning a deserialized account, given an address.
* The namespace provides handles to an [[AccountClient]] object for each
* account in a program.
*
* ## Usage
*
* ```javascript
* account.<account-client>
* ```
*
* ## Example
*
* To fetch a `Counter` account from the above example,
*
* ```javascript
* const counter = await program.account.counter.fetch(address);
* ```
*
* For the full API, see the [[AccountClient]] reference.
*/
export type AccountFn<T = any> = AccountProps & ((address: PublicKey) => T);
export interface AccountNamespace {
[key: string]: AccountClient;
}
/**
* Non function properties on the acccount namespace.
*/
type AccountProps = {
size: number;
all: (filter?: Buffer) => Promise<ProgramAccount<any>[]>;
subscribe: (address: Address, commitment?: Commitment) => EventEmitter;
unsubscribe: (address: Address) => void;
createInstruction: (signer: Signer) => Promise<TransactionInstruction>;
associated: (...args: PublicKey[]) => Promise<any>;
associatedAddress: (...args: PublicKey[]) => Promise<PublicKey>;
};
export class AccountClient {
/**
* Returns the number of bytes in this account.
*/
get size(): number {
return this._size;
}
private _size: number;
/**
* Returns the program ID owning all accounts.
*/
get programId(): PublicKey {
return this._programId;
}
private _programId: PublicKey;
/**
* Returns the cleint's wallet and network provider.
*/
get provider(): Provider {
return this._provider;
}
private _provider: Provider;
/**
* Returns the coder.
*/
get coder(): Coder {
return this._coder;
}
private _coder: Coder;
private _idlAccount: IdlTypeDef;
constructor(
idl: Idl,
idlAccount: IdlTypeDef,
programId: PublicKey,
provider?: Provider,
coder?: Coder
) {
this._idlAccount = idlAccount;
this._programId = programId;
this._provider = provider ?? getProvider();
this._coder = coder ?? new Coder(idl);
this._size = ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount);
}
/**
* Returns a deserialized account.
*
* @param address The address of the account to fetch.
*/
async fetch(address: Address): Promise<Object> {
const accountInfo = await this._provider.connection.getAccountInfo(
translateAddress(address)
);
if (accountInfo === null) {
throw new Error(`Account does not exist ${address.toString()}`);
}
// Assert the account discriminator is correct.
const discriminator = await accountDiscriminator(this._idlAccount.name);
if (discriminator.compare(accountInfo.data.slice(0, 8))) {
throw new Error("Invalid account discriminator");
}
return this._coder.accounts.decode(this._idlAccount.name, accountInfo.data);
}
/**
* Returns all instances of this account type for the program.
*/
async all(filter?: Buffer): Promise<ProgramAccount<any>[]> {
let bytes = await accountDiscriminator(this._idlAccount.name);
if (filter !== undefined) {
bytes = Buffer.concat([bytes, filter]);
}
let resp = await this._provider.connection.getProgramAccounts(
this._programId,
{
commitment: this._provider.connection.commitment,
filters: [
{
memcmp: {
offset: 0,
bytes: bs58.encode(bytes),
},
},
],
}
);
return resp.map(({ pubkey, account }) => {
return {
publicKey: pubkey,
account: this._coder.accounts.decode(
this._idlAccount.name,
account.data
),
};
});
}
/**
* Returns an `EventEmitter` emitting a "change" event whenever the account
* changes.
*/
subscribe(address: Address, commitment?: Commitment): EventEmitter {
if (subscriptions.get(address.toString())) {
return subscriptions.get(address.toString()).ee;
}
const ee = new EventEmitter();
address = translateAddress(address);
const listener = this._provider.connection.onAccountChange(
address,
(acc) => {
const account = this._coder.accounts.decode(
this._idlAccount.name,
acc.data
);
ee.emit("change", account);
},
commitment
);
subscriptions.set(address.toString(), {
ee,
listener,
});
return ee;
}
/**
* Unsubscribes from the account at the given address.
*/
unsubscribe(address: Address) {
let sub = subscriptions.get(address.toString());
if (!sub) {
console.warn("Address is not subscribed");
return;
}
if (subscriptions) {
this._provider.connection
.removeAccountChangeListener(sub.listener)
.then(() => {
subscriptions.delete(address.toString());
})
.catch(console.error);
}
}
/**
* Returns an instruction for creating this account.
*/
async createInstruction(
signer: Signer,
sizeOverride?: number
): Promise<TransactionInstruction> {
const size = this.size;
return SystemProgram.createAccount({
fromPubkey: this._provider.wallet.publicKey,
newAccountPubkey: signer.publicKey,
space: sizeOverride ?? size,
lamports: await this._provider.connection.getMinimumBalanceForRentExemption(
sizeOverride ?? size
),
programId: this._programId,
});
}
/**
* Function returning the associated account. Args are keys to associate.
* Order matters.
*/
async associated(...args: PublicKey[]): Promise<any> {
const addr = await this.associatedAddress(...args);
return await this.fetch(addr);
}
/**
* Function returning the associated address. Args are keys to associate.
* Order matters.
*/
async associatedAddress(...args: PublicKey[]): Promise<PublicKey> {
let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
args.forEach((arg) => {
seeds.push(translateAddress(arg).toBuffer());
});
const [assoc] = await PublicKey.findProgramAddress(seeds, this._programId);
return assoc;
}
}
/**
* @hidden
@ -55,176 +276,3 @@ export type ProgramAccount<T = any> = {
// Tracks all subscriptions.
const subscriptions: Map<string, Subscription> = new Map();
export default class AccountFactory {
// Returns the generated accounts namespace.
public static build(
idl: Idl,
coder: Coder,
programId: PublicKey,
provider: Provider
): AccountNamespace {
const accountFns: AccountNamespace = {};
idl.accounts.forEach((idlAccount) => {
const name = camelCase(idlAccount.name);
// Fetches the decoded account from the network.
const accountsNamespace = async (address: Address): Promise<any> => {
const accountInfo = await provider.connection.getAccountInfo(
translateAddress(address)
);
if (accountInfo === null) {
throw new Error(`Account does not exist ${address.toString()}`);
}
// Assert the account discriminator is correct.
const discriminator = await accountDiscriminator(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 (
signer: Signer,
sizeOverride?: number
): Promise<TransactionInstruction> => {
// @ts-ignore
const size = accountsNamespace["size"];
return SystemProgram.createAccount({
fromPubkey: provider.wallet.publicKey,
newAccountPubkey: signer.publicKey,
space: sizeOverride ?? size,
lamports: await provider.connection.getMinimumBalanceForRentExemption(
sizeOverride ?? size
),
programId,
});
};
// Subscribes to all changes to this account.
// @ts-ignore
accountsNamespace["subscribe"] = (
address: Address,
commitment?: Commitment
): EventEmitter => {
if (subscriptions.get(address.toString())) {
return subscriptions.get(address.toString()).ee;
}
const ee = new EventEmitter();
address = translateAddress(address);
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: Address) => {
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: Address[]
): Promise<PublicKey> => {
let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
args.forEach((arg) => {
seeds.push(translateAddress(arg).toBuffer());
});
const [assoc] = await PublicKey.findProgramAddress(seeds, programId);
return assoc;
};
// Function returning the associated account. Args are keys to associate.
// Order matters.
accountsNamespace["associated"] = async (
...args: Address[]
): Promise<any> => {
const addr = await accountsNamespace["associatedAddress"](...args);
return await accountsNamespace(addr);
};
accountFns[name] = accountsNamespace;
});
return accountFns;
}
}

View File

@ -3,21 +3,21 @@ import { PublicKey } from "@solana/web3.js";
import Coder from "../../coder";
import Provider from "../../provider";
import { Idl } from "../../idl";
import { parseIdlErrors } from "../common";
import StateFactory, { StateNamespace } from "./state";
import StateFactory, { StateClient } from "./state";
import InstructionFactory, { InstructionNamespace } from "./instruction";
import TransactionFactory, { TransactionNamespace } from "./transaction";
import RpcFactory, { RpcNamespace } from "./rpc";
import AccountFactory, { AccountNamespace } from "./account";
import SimulateFactory, { SimulateNamespace } from "./simulate";
import { parseIdlErrors } from "../common";
// Re-exports.
export { StateNamespace } from "./state";
export { InstructionNamespace } from "./instruction";
export { TransactionNamespace, TxFn } from "./transaction";
export { StateClient } from "./state";
export { InstructionNamespace, InstructionFn } from "./instruction";
export { TransactionNamespace, TransactionFn } from "./transaction";
export { RpcNamespace, RpcFn } from "./rpc";
export { AccountNamespace, AccountFn, ProgramAccount } from "./account";
export { SimulateNamespace } from "./simulate";
export { AccountNamespace, AccountClient, ProgramAccount } from "./account";
export { SimulateNamespace, SimulateFn } from "./simulate";
export default class NamespaceFactory {
/**
@ -33,26 +33,24 @@ export default class NamespaceFactory {
InstructionNamespace,
TransactionNamespace,
AccountNamespace,
StateNamespace,
SimulateNamespace
SimulateNamespace,
StateClient
] {
const idlErrors = parseIdlErrors(idl);
const rpc: RpcNamespace = {};
const instruction: InstructionNamespace = {};
const transaction: TransactionNamespace = {};
const simulate: SimulateNamespace = {};
const state = StateFactory.build(
idl,
coder,
programId,
idlErrors,
provider
);
const idlErrors = parseIdlErrors(idl);
const state = StateFactory.build(idl, coder, programId, provider);
idl.instructions.forEach((idlIx) => {
const ixItem = InstructionFactory.build(idlIx, coder, programId);
const ixItem = InstructionFactory.build(
idlIx,
(ixName: string, ix: any) => coder.instruction.encode(ixName, ix),
programId
);
const txItem = TransactionFactory.build(idlIx, ixItem);
const rpcItem = RpcFactory.build(idlIx, txItem, idlErrors, provider);
const simulateItem = SimulateFactory.build(
@ -77,6 +75,6 @@ export default class NamespaceFactory {
? AccountFactory.build(idl, coder, programId, provider)
: {};
return [rpc, instruction, transaction, account, state, simulate];
return [rpc, instruction, transaction, account, simulate, state];
}
}

View File

@ -1,7 +1,6 @@
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
import { IdlAccount, IdlInstruction, IdlAccountItem } from "../../idl";
import { IdlError } from "../../error";
import Coder from "../../coder";
import {
toInstruction,
validateAccounts,
@ -10,28 +9,12 @@ import {
} from "../common";
import { Accounts, splitArgsAndCtx } from "../context";
/**
* Dynamically generated instruction namespace.
*/
export interface InstructionNamespace {
[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: Accounts) => any;
};
export default class InstructionNamespaceFactory {
// Builds the instuction namespace.
public static build(
idlIx: IdlInstruction,
coder: Coder,
encodeFn: InstructionEncodeFn,
programId: PublicKey
): IxFn {
): InstructionFn {
if (idlIx.name === "_inner") {
throw new IdlError("the _inner name is reserved");
}
@ -41,10 +24,7 @@ export default class InstructionNamespaceFactory {
validateAccounts(idlIx.accounts, ctx.accounts);
validateInstruction(idlIx, ...args);
const keys = InstructionNamespaceFactory.accountsArray(
ctx.accounts,
idlIx.accounts
);
const keys = ix.accounts(ctx.accounts);
if (ctx.remainingAccounts !== undefined) {
keys.push(...ctx.remainingAccounts);
@ -56,10 +36,7 @@ export default class InstructionNamespaceFactory {
return new TransactionInstruction({
keys,
programId,
data: coder.instruction.encode(
idlIx.name,
toInstruction(idlIx, ...ixArgs)
),
data: encodeFn(idlIx.name, toInstruction(idlIx, ...ixArgs)),
});
};
@ -96,6 +73,51 @@ export default class InstructionNamespaceFactory {
}
}
/**
* The namespace provides functions to build [[TransactionInstruction]]
* objects for each method of a program.
*
* ## Usage
*
* ```javascript
* instruction.<method>(...args, ctx);
* ```
*
* ## Parameters
*
* 1. `args` - The positional arguments for the program. The type and number
* of these arguments depend on the program being used.
* 2. `ctx` - [[Context]] non-argument parameters to pass to the method.
* Always the last parameter in the method call.
*
* ## Example
*
* To create an instruction for the `increment` method above,
*
* ```javascript
* const tx = await program.instruction.increment({
* accounts: {
* counter,
* },
* });
* ```
*/
export interface InstructionNamespace {
[key: string]: InstructionFn;
}
/**
* Function to create a `TransactionInstruction` generated from an IDL.
* Additionally it provides an `accounts` utility method, returning a list
* of ordered accounts for the instruction.
*/
export type InstructionFn = IxProps & ((...args: any[]) => any);
type IxProps = {
accounts: (ctx: Accounts) => any;
};
export type InstructionEncodeFn = (ixName: string, ix: any) => Buffer;
// Throws error if any argument required for the `ix` is not given.
function validateInstruction(ix: IdlInstruction, ...args: any[]) {
// todo

View File

@ -3,25 +3,12 @@ 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 RpcNamespace {
[key: string]: RpcFn;
}
/**
* RpcFn is a single rpc method generated from an IDL.
*/
export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;
import { TransactionFn } from "./transaction";
export default class RpcFactory {
// Builds the rpc namespace.
public static build(
idlIx: IdlInstruction,
txFn: TxFn,
txFn: TransactionFn,
idlErrors: Map<number, string>,
provider: Provider
): RpcFn {
@ -44,3 +31,47 @@ export default class RpcFactory {
return rpc;
}
}
/**
* The namespace provides async methods to send signed transactions for each
* *non*-state method on Anchor program.
*
* Keys are method names, values are RPC functions returning a
* [[TransactionInstruction]].
*
* ## Usage
*
* ```javascript
* rpc.<method>(...args, ctx);
* ```
*
* ## Parameters
*
* 1. `args` - The positional arguments for the program. The type and number
* of these arguments depend on the program being used.
* 2. `ctx` - [[Context]] non-argument parameters to pass to the method.
* Always the last parameter in the method call.
* ```
*
* ## Example
*
* To send a transaction invoking the `increment` method above,
*
* ```javascript
* const txSignature = await program.rpc.increment({
* accounts: {
* counter,
* authority,
* },
* });
* ```
*/
export interface RpcNamespace {
[key: string]: RpcFn;
}
/**
* RpcFn is a single RPC method generated from an IDL, sending a transaction
* paid for and signed by the configured provider.
*/
export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;

View File

@ -3,33 +3,15 @@ import Provider from "../../provider";
import { IdlInstruction } from "../../idl";
import { translateError } from "../common";
import { splitArgsAndCtx } from "../context";
import { TxFn } from "./transaction";
import { TransactionFn } from "./transaction";
import { EventParser } from "../event";
import Coder from "../../coder";
import { Idl } from "../../idl";
/**
* Dynamically generated simualte namespace.
*/
export interface SimulateNamespace {
[key: string]: SimulateFn;
}
/**
* RpcFn is a single rpc method generated from an IDL.
*/
export type SimulateFn = (...args: any[]) => Promise<SimulateResponse>;
type SimulateResponse = {
events: Event[];
raw: string[];
};
export default class SimulateFactory {
// Builds the rpc namespace.
public static build(
idlIx: IdlInstruction,
txFn: TxFn,
txFn: TransactionFn,
idlErrors: Map<number, string>,
provider: Provider,
coder: Coder,
@ -74,3 +56,54 @@ export default class SimulateFactory {
return simulate;
}
}
/**
* The namespace provides functions to simulate transactions for each method
* of a program, returning a list of deserialized events *and* raw program
* logs.
*
* One can use this to read data calculated from a program on chain, by
* emitting an event in the program and reading the emitted event client side
* via the `simulate` namespace.
*
* ## Usage
*
* ```javascript
* program.simulate.<method>(...args, ctx);
* ```
*
* ## Parameters
*
* 1. `args` - The positional arguments for the program. The type and number
* of these arguments depend on the program being used.
* 2. `ctx` - [[Context]] non-argument parameters to pass to the method.
* Always the last parameter in the method call.
*
* ## Example
*
* To simulate the `increment` method above,
*
* ```javascript
* const events = await program.simulate.increment({
* accounts: {
* counter,
* },
* });
* ```
*/
export interface SimulateNamespace {
[key: string]: SimulateFn;
}
/**
* RpcFn is a single method generated from an IDL. It simulates a method
* against a cluster configured by the provider, returning a list of all the
* events and raw logs that were emitted during the execution of the
* method.
*/
export type SimulateFn = (...args: any[]) => Promise<SimulateResponse>;
type SimulateResponse = {
events: Event[];
raw: string[];
};

View File

@ -1,176 +1,224 @@
import EventEmitter from "eventemitter3";
import camelCase from "camelcase";
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 { RpcNamespace, InstructionNamespace } from "./";
import {
Subscription,
translateError,
toInstruction,
validateAccounts,
} from "../common";
import { Accounts, splitArgsAndCtx } from "../context";
import { RpcNamespace, InstructionNamespace, TransactionNamespace } from "./";
import { getProvider } from "../../";
import { Subscription, validateAccounts, parseIdlErrors } from "../common";
import { findProgramAddressSync, createWithSeedSync } from "../../utils/pubkey";
import { Accounts } from "../context";
import InstructionNamespaceFactory from "./instruction";
export type StateNamespace = () =>
| Promise<any>
| {
address: () => Promise<PublicKey>;
rpc: RpcNamespace;
instruction: InstructionNamespace;
subscribe: (commitment?: Commitment) => EventEmitter;
unsubscribe: () => void;
};
import RpcNamespaceFactory from "./rpc";
import TransactionNamespaceFactory from "./transaction";
export default class StateFactory {
// Builds the state namespace.
public static build(
idl: Idl,
coder: Coder,
programId: PublicKey,
idlErrors: Map<number, string>,
provider: Provider
): StateNamespace | undefined {
): StateClient | undefined {
if (idl.state === undefined) {
return undefined;
}
return new StateClient(idl, programId, provider, coder);
}
}
// 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);
};
/**
* 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.
*/
export class StateClient {
/**
* [[RpcNamespace]] for all state methods.
*/
readonly rpc: RpcNamespace;
// Namespace with all rpc functions.
const rpc: RpcNamespace = {};
const ix: InstructionNamespace = {};
/**
* [[InstructionNamespace]] for all state methods.
*/
readonly instruction: InstructionNamespace;
idl.state.methods.forEach((m: IdlStateMethod) => {
const accounts = async (accounts: Accounts): Promise<any> => {
const keys = await stateInstructionKeys(
programId,
provider,
/**
* [[TransactionNamespace]] for all state methods.
*/
readonly transaction: TransactionNamespace;
/**
* Returns the program ID owning the state.
*/
get programId(): PublicKey {
return this._programId;
}
private _programId: PublicKey;
/**
* Returns the client's wallet and network provider.
*/
get provider(): Provider {
return this._provider;
}
private _provider: Provider;
/**
* Returns the coder.
*/
get coder(): Coder {
return this._coder;
}
private _address: PublicKey;
private _coder: Coder;
private _idl: Idl;
private _sub: Subscription | null;
constructor(
idl: Idl,
programId: PublicKey,
provider?: Provider,
coder?: Coder
) {
this._idl = idl;
this._programId = programId;
this._address = programStateAddress(programId);
this._provider = provider ?? getProvider();
this._coder = coder ?? new Coder(idl);
this._sub = null;
// Build namespaces.
const [instruction, transaction, rpc] = ((): [
InstructionNamespace,
TransactionNamespace,
RpcNamespace
] => {
let instruction: InstructionNamespace = {};
let transaction: TransactionNamespace = {};
let rpc: RpcNamespace = {};
idl.state.methods.forEach((m: IdlStateMethod) => {
// Build instruction method.
const ixItem = InstructionNamespaceFactory.build(
m,
accounts
(ixName: string, ix: any) =>
coder.instruction.encodeState(ixName, ix),
programId
);
return keys.concat(
InstructionNamespaceFactory.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,
ixItem["accounts"] = (accounts: Accounts) => {
const keys = stateInstructionKeys(programId, provider, m, accounts);
return keys.concat(
InstructionNamespaceFactory.accountsArray(accounts, m.accounts)
);
};
// Build transaction method.
const txItem = TransactionNamespaceFactory.build(m, ixItem);
// Build RPC method.
const rpcItem = RpcNamespaceFactory.build(
m,
txItem,
parseIdlErrors(idl),
provider
);
// Attach them all to their respective namespaces.
const name = camelCase(m.name);
instruction[name] = ixItem;
transaction[name] = txItem;
rpc[name] = rpcItem;
});
return ee;
return [instruction, transaction, rpc];
})();
this.instruction = instruction;
this.transaction = transaction;
this.rpc = rpc;
}
/**
* Returns the deserialized state account.
*/
async fetch(): Promise<Object> {
const addr = this.address();
const accountInfo = await this.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(
this._idl.state.struct.name
);
if (expectedDiscriminator.compare(accountInfo.data.slice(0, 8))) {
throw new Error("Invalid account discriminator");
}
return this.coder.state.decode(accountInfo.data);
}
/**
* Returns the state address.
*/
address(): PublicKey {
return this._address;
}
/**
* Returns an `EventEmitter` with a `"change"` event that's fired whenever
* the state account cahnges.
*/
subscribe(commitment?: Commitment): EventEmitter {
if (this._sub !== null) {
return this._sub.ee;
}
const ee = new EventEmitter();
const listener = this.provider.connection.onAccountChange(
this.address(),
(acc) => {
const account = this.coder.state.decode(acc.data);
ee.emit("change", account);
},
commitment
);
this._sub = {
ee,
listener,
};
// Unsubscribe from account changes.
state["unsubscribe"] = () => {
if (sub !== null) {
provider.connection
.removeAccountChangeListener(sub.listener)
.then(async () => {
sub = null;
})
.catch(console.error);
}
};
return ee;
}
return state;
/**
* Unsubscribes to state changes.
*/
unsubscribe() {
if (this._sub !== null) {
this.provider.connection
.removeAccountChangeListener(this._sub.listener)
.then(async () => {
this._sub = null;
})
.catch(console.error);
}
}
}
// Calculates the deterministic address of the program's "state" account.
async function programStateAddress(programId: PublicKey): Promise<PublicKey> {
let [registrySigner] = await PublicKey.findProgramAddress([], programId);
return PublicKey.createWithSeed(registrySigner, "unversioned", programId);
function programStateAddress(programId: PublicKey): PublicKey {
let [registrySigner] = findProgramAddressSync([], programId);
return createWithSeedSync(registrySigner, "unversioned", programId);
}
// Returns the common keys that are prepended to all instructions targeting
// the "state" of a program.
async function stateInstructionKeys(
function stateInstructionKeys(
programId: PublicKey,
provider: Provider,
m: IdlStateMethod,
@ -178,7 +226,7 @@ async function stateInstructionKeys(
) {
if (m.name === "new") {
// Ctor `new` method.
const [programSigner] = await PublicKey.findProgramAddress([], programId);
const [programSigner] = findProgramAddressSync([], programId);
return [
{
pubkey: provider.wallet.publicKey,
@ -186,7 +234,7 @@ async function stateInstructionKeys(
isSigner: true,
},
{
pubkey: await programStateAddress(programId),
pubkey: programStateAddress(programId),
isWritable: true,
isSigner: false,
},
@ -208,7 +256,7 @@ async function stateInstructionKeys(
validateAccounts(m.accounts, accounts);
return [
{
pubkey: await programStateAddress(programId),
pubkey: programStateAddress(programId),
isWritable: true,
isSigner: false,
},

View File

@ -1,23 +1,13 @@
import { Transaction } from "@solana/web3.js";
import { IdlInstruction } from "../../idl";
import { splitArgsAndCtx } from "../context";
import { IxFn } from "./instruction";
/**
* Dynamically generated transaction namespace.
*/
export interface TransactionNamespace {
[key: string]: TxFn;
}
/**
* Tx is a function to create a `Transaction` generate from an IDL.
*/
export type TxFn = (...args: any[]) => Transaction;
import { InstructionFn } from "./instruction";
export default class TransactionFactory {
// Builds the transaction namespace.
public static build(idlIx: IdlInstruction, ixFn: IxFn): TxFn {
public static build(
idlIx: IdlInstruction,
ixFn: InstructionFn
): TransactionFn {
const txFn = (...args: any[]): Transaction => {
const [, ctx] = splitArgsAndCtx(idlIx, [...args]);
const tx = new Transaction();
@ -31,3 +21,41 @@ export default class TransactionFactory {
return txFn;
}
}
/**
* The namespace provides functions to build [[Transaction]] objects for each
* method of a program.
*
* ## Usage
*
* ```javascript
* program.transaction.<method>(...args, ctx);
* ```
*
* ## Parameters
*
* 1. `args` - The positional arguments for the program. The type and number
* of these arguments depend on the program being used.
* 2. `ctx` - [[Context]] non-argument parameters to pass to the method.
* Always the last parameter in the method call.
*
* ## Example
*
* To create an instruction for the `increment` method above,
*
* ```javascript
* const tx = await program.transaction.increment({
* accounts: {
* counter,
* },
* });
* ```
*/
export interface TransactionNamespace {
[key: string]: TransactionFn;
}
/**
* Tx is a function to create a `Transaction` for a given program instruction.
*/
export type TransactionFn = (...args: any[]) => Transaction;

14
ts/src/utils/index.ts Normal file
View File

@ -0,0 +1,14 @@
import { sha256 } from "crypto-hash";
import * as bs58 from "bs58";
import * as rpc from "./rpc";
import * as publicKey from "./pubkey";
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);
}
export default { sha256, bs58, rpc, publicKey };

78
ts/src/utils/pubkey.ts Normal file
View File

@ -0,0 +1,78 @@
import BN from "bn.js";
import { sha256 as sha256Sync } from "js-sha256";
import { PublicKey } from "@solana/web3.js";
// Sync version of web3.PublicKey.createWithSeed.
export function createWithSeedSync(
fromPublicKey: PublicKey,
seed: string,
programId: PublicKey
): PublicKey {
const buffer = Buffer.concat([
fromPublicKey.toBuffer(),
Buffer.from(seed),
programId.toBuffer(),
]);
const hash = sha256Sync.digest(buffer);
return new PublicKey(Buffer.from(hash));
}
// Sync version of web3.PublicKey.createProgramAddress.
export function createProgramAddressSync(
seeds: Array<Buffer | Uint8Array>,
programId: PublicKey
): PublicKey {
const MAX_SEED_LENGTH = 32;
let buffer = Buffer.alloc(0);
seeds.forEach(function (seed) {
if (seed.length > MAX_SEED_LENGTH) {
throw new TypeError(`Max seed length exceeded`);
}
buffer = Buffer.concat([buffer, toBuffer(seed)]);
});
buffer = Buffer.concat([
buffer,
programId.toBuffer(),
Buffer.from("ProgramDerivedAddress"),
]);
let hash = sha256Sync(new Uint8Array(buffer));
let publicKeyBytes = new BN(hash, 16).toArray(undefined, 32);
if (PublicKey.isOnCurve(new Uint8Array(publicKeyBytes))) {
throw new Error(`Invalid seeds, address must fall off the curve`);
}
return new PublicKey(publicKeyBytes);
}
// Sync version of web3.PublicKey.findProgramAddress.
export function findProgramAddressSync(
seeds: Array<Buffer | Uint8Array>,
programId: PublicKey
): [PublicKey, number] {
let nonce = 255;
let address: PublicKey | undefined;
while (nonce != 0) {
try {
const seedsWithNonce = seeds.concat(Buffer.from([nonce]));
address = createProgramAddressSync(seedsWithNonce, programId);
} catch (err) {
if (err instanceof TypeError) {
throw err;
}
nonce--;
continue;
}
return [address, nonce];
}
throw new Error(`Unable to find a viable program address nonce`);
}
const toBuffer = (arr: Buffer | Uint8Array | Array<number>): Buffer => {
if (arr instanceof Buffer) {
return arr;
} else if (arr instanceof Uint8Array) {
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
} else {
return Buffer.from(arr);
}
};

View File

@ -1,14 +1,7 @@
import * as bs58 from "bs58";
import { sha256 } from "crypto-hash";
import assert from "assert";
import { PublicKey, AccountInfo, Connection } from "@solana/web3.js";
import { idlAddress } from "./idl";
export const TOKEN_PROGRAM_ID = new PublicKey(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
);
async function getMultipleAccounts(
export async function getMultipleAccounts(
connection: Connection,
publicKeys: PublicKey[]
): Promise<
@ -68,20 +61,3 @@ 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,
getMultipleAccounts,
idlAddress,
};
export default utils;