From 2f780e0d274f47e442b3f0d107db805a41c6def0 Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Tue, 25 May 2021 20:04:05 -0700 Subject: [PATCH] ts: Reorganize program namespaces into well typed elements (#322) --- CHANGELOG.md | 5 +- .../cashiers-check/tests/cashiers-check.js | 4 +- examples/chat/tests/chat.js | 4 +- examples/composite/tests/composite.js | 4 +- examples/interface/tests/interface.js | 4 +- examples/lockup/tests/lockup.js | 34 +- examples/misc/tests/misc.js | 18 +- examples/multisig/tests/multisig.js | 6 +- examples/tutorial/basic-1/tests/basic-1.js | 8 +- examples/tutorial/basic-2/tests/basic-2.js | 4 +- examples/tutorial/basic-3/tests/basic-3.js | 2 +- examples/tutorial/basic-4/tests/basic-4.js | 4 +- examples/zero-copy/tests/zero-copy.js | 18 +- ts/package.json | 2 +- ts/src/coder.ts | 571 ------------------ ts/src/coder/accounts.ts | 55 ++ ts/src/coder/common.ts | 113 ++++ ts/src/coder/event.ts | 67 ++ ts/src/coder/idl.ts | 154 +++++ ts/src/coder/index.ts | 62 ++ ts/src/coder/instruction.ts | 76 +++ ts/src/coder/state.ts | 36 ++ ts/src/coder/types.ts | 38 ++ ts/src/idl.ts | 4 +- ts/src/index.ts | 45 +- ts/src/program/common.ts | 2 - ts/src/program/index.ts | 66 +- ts/src/program/namespace/account.ts | 436 +++++++------ ts/src/program/namespace/index.ts | 38 +- ts/src/program/namespace/instruction.ts | 76 ++- ts/src/program/namespace/rpc.ts | 61 +- ts/src/program/namespace/simulate.ts | 73 ++- ts/src/program/namespace/state.ts | 320 +++++----- ts/src/program/namespace/transaction.ts | 58 +- ts/src/utils/index.ts | 14 + ts/src/utils/pubkey.ts | 78 +++ ts/src/{utils.ts => utils/rpc.ts} | 26 +- 37 files changed, 1469 insertions(+), 1117 deletions(-) delete mode 100644 ts/src/coder.ts create mode 100644 ts/src/coder/accounts.ts create mode 100644 ts/src/coder/common.ts create mode 100644 ts/src/coder/event.ts create mode 100644 ts/src/coder/idl.ts create mode 100644 ts/src/coder/index.ts create mode 100644 ts/src/coder/instruction.ts create mode 100644 ts/src/coder/state.ts create mode 100644 ts/src/coder/types.ts create mode 100644 ts/src/utils/index.ts create mode 100644 ts/src/utils/pubkey.ts rename ts/src/{utils.ts => utils/rpc.ts} (72%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e0aada09..1a4c5c6a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,9 +19,10 @@ incremented for features. ## Breaking Changes +* ts: Retrieving deserialized accounts from the `.account.` and `.state` namespaces now require explicitly invoking the `fetch` API. For example, `program.account.myAccount()` and `program.state()` is now `program.account.myAccount.fetch(
)` 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 diff --git a/examples/cashiers-check/tests/cashiers-check.js b/examples/cashiers-check/tests/cashiers-check.js index bd1270601..60b372668 100644 --- a/examples/cashiers-check/tests/cashiers-check.js +++ b/examples/cashiers-check/tests/cashiers-check.js @@ -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( diff --git a/examples/chat/tests/chat.js b/examples/chat/tests/chat.js index 2e5be183d..19b444744 100644 --- a/examples/chat/tests/chat.js +++ b/examples/chat/tests/chat.js @@ -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); diff --git a/examples/composite/tests/composite.js b/examples/composite/tests/composite.js index 1913806a9..67b7197b4 100644 --- a/examples/composite/tests/composite.js +++ b/examples/composite/tests/composite.js @@ -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))); diff --git a/examples/interface/tests/interface.js b/examples/interface/tests/interface.js index 7696062cd..a0e33413b 100644 --- a/examples/interface/tests/interface.js +++ b/examples/interface/tests/interface.js @@ -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))); }); }); diff --git a/examples/lockup/tests/lockup.js b/examples/lockup/tests/lockup.js index 91e91300a..3a3831a85 100644 --- a/examples/lockup/tests/lockup.js +++ b/examples/lockup/tests/lockup.js @@ -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 ); diff --git a/examples/misc/tests/misc.js b/examples/misc/tests/misc.js index a806fd59b..1c55ea0d3 100644 --- a/examples/misc/tests/misc.js +++ b/examples/misc/tests/misc.js @@ -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); }); }); diff --git a/examples/multisig/tests/multisig.js b/examples/multisig/tests/multisig.js index 1662ed98e..90a7cb96e 100644 --- a/examples/multisig/tests/multisig.js +++ b/examples/multisig/tests/multisig.js @@ -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))); diff --git a/examples/tutorial/basic-1/tests/basic-1.js b/examples/tutorial/basic-1/tests/basic-1.js index d4694da49..f4fd460e2 100644 --- a/examples/tutorial/basic-1/tests/basic-1.js +++ b/examples/tutorial/basic-1/tests/basic-1.js @@ -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))); diff --git a/examples/tutorial/basic-2/tests/basic-2.js b/examples/tutorial/basic-2/tests/basic-2.js index 6c20a2429..67093ec9c 100644 --- a/examples/tutorial/basic-2/tests/basic-2.js +++ b/examples/tutorial/basic-2/tests/basic-2.js @@ -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) diff --git a/examples/tutorial/basic-3/tests/basic-3.js b/examples/tutorial/basic-3/tests/basic-3.js index 9832f83eb..ac6db1e22 100644 --- a/examples/tutorial/basic-3/tests/basic-3.js +++ b/examples/tutorial/basic-3/tests/basic-3.js @@ -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))); }); }); diff --git a/examples/tutorial/basic-4/tests/basic-4.js b/examples/tutorial/basic-4/tests/basic-4.js index 5a8f68452..509519a1b 100644 --- a/examples/tutorial/basic-4/tests/basic-4.js +++ b/examples/tutorial/basic-4/tests/basic-4.js @@ -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))); }); }); diff --git a/examples/zero-copy/tests/zero-copy.js b/examples/zero-copy/tests/zero-copy.js index b27b4eb90..3411bb2ae 100644 --- a/examples/zero-copy/tests/zero-copy.js +++ b/examples/zero-copy/tests/zero-copy.js @@ -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) { diff --git a/ts/package.json b/ts/package.json index 346aadfbb..05110902d 100644 --- a/ts/package.json +++ b/ts/package.json @@ -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", diff --git a/ts/src/coder.ts b/ts/src/coder.ts deleted file mode 100644 index bbf57f2b7..000000000 --- a/ts/src/coder.ts +++ /dev/null @@ -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; - - 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 { - 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; - - 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( - accountName: string, - account: T - ): Promise { - 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(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; - - 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(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(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; - - /** - * Maps base64 encoded event discriminator to event name. - */ - private discriminators: Map; - - 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( - 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(name: string, account: T): Promise { - 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(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 { - // @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 { - // @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); -} diff --git a/ts/src/coder/accounts.ts b/ts/src/coder/accounts.ts new file mode 100644 index 000000000..0eb9f6ac1 --- /dev/null +++ b/ts/src/coder/accounts.ts @@ -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; + + 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( + accountName: string, + account: T + ): Promise { + 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(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 { + return Buffer.from(sha256.digest(`account:${name}`)).slice(0, 8); +} diff --git a/ts/src/coder/common.ts b/ts/src/coder/common.ts new file mode 100644 index 000000000..cc93f8030 --- /dev/null +++ b/ts/src/coder/common.ts @@ -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); +} diff --git a/ts/src/coder/event.ts b/ts/src/coder/event.ts new file mode 100644 index 000000000..3b73a1006 --- /dev/null +++ b/ts/src/coder/event.ts @@ -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; + + /** + * Maps base64 encoded event discriminator to event name. + */ + private discriminators: Map; + + 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( + 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); +} diff --git a/ts/src/coder/idl.ts b/ts/src/coder/idl.ts new file mode 100644 index 000000000..30be64a99 --- /dev/null +++ b/ts/src/coder/idl.ts @@ -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}`); + } + } +} diff --git a/ts/src/coder/index.ts b/ts/src/coder/index.ts new file mode 100644 index 000000000..e0ececbe4 --- /dev/null +++ b/ts/src/coder/index.ts @@ -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); + } +} diff --git a/ts/src/coder/instruction.ts b/ts/src/coder/instruction.ts new file mode 100644 index 000000000..d37145884 --- /dev/null +++ b/ts/src/coder/instruction.ts @@ -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; + + 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 { + 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); + } +} diff --git a/ts/src/coder/state.ts b/ts/src/coder/state.ts new file mode 100644 index 000000000..293e7e6ef --- /dev/null +++ b/ts/src/coder/state.ts @@ -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(name: string, account: T): Promise { + 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(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 { + return Buffer.from(sha256.digest(`state:${name}`)).slice(0, 8); +} diff --git a/ts/src/coder/types.ts b/ts/src/coder/types.ts new file mode 100644 index 000000000..da3807b6d --- /dev/null +++ b/ts/src/coder/types.ts @@ -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; + + 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(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(accountName: string, ix: Buffer): T { + const layout = this.layouts.get(accountName); + return layout.decode(ix); + } +} diff --git a/ts/src/idl.ts b/ts/src/idl.ts index 8ff025440..95aab38c8 100644 --- a/ts/src/idl.ts +++ b/ts/src/idl.ts @@ -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" diff --git a/ts/src/index.ts b/ts/src/index.ts index 964aa7375..366136559 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -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, diff --git a/ts/src/program/common.ts b/ts/src/program/common.ts index 8e902f51a..74640c72c 100644 --- a/ts/src/program/common.ts +++ b/ts/src/program/common.ts @@ -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; diff --git a/ts/src/program/index.ts b/ts/src/program/index.ts index 99f15f627..69c0cf403 100644 --- a/ts/src/program/index.ts +++ b/ts/src/program/index.ts @@ -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.. + * program.. * ``` * * 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.(...args, ctx); + * rpc.(...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.(address); + * program.account. * ``` * - * ## 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.(...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.(...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; } /** diff --git a/ts/src/program/namespace/account.ts b/ts/src/program/namespace/account.ts index cc3aa2b66..c54001cdd 100644 --- a/ts/src/program/namespace/account.ts +++ b/ts/src/program/namespace/account.ts @@ -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. + * ``` + * + * ## 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 = 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[]>; - subscribe: (address: Address, commitment?: Commitment) => EventEmitter; - unsubscribe: (address: Address) => void; - createInstruction: (signer: Signer) => Promise; - associated: (...args: PublicKey[]) => Promise; - associatedAddress: (...args: PublicKey[]) => Promise; -}; +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 { + 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[]> { + 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 { + 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 { + 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 { + 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 = { // Tracks all subscriptions. const subscriptions: Map = 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 => { - 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 => { - // @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[]> => { - 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 => { - 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 => { - const addr = await accountsNamespace["associatedAddress"](...args); - return await accountsNamespace(addr); - }; - - accountFns[name] = accountsNamespace; - }); - - return accountFns; - } -} diff --git a/ts/src/program/namespace/index.ts b/ts/src/program/namespace/index.ts index 3f7474097..d3443a912 100644 --- a/ts/src/program/namespace/index.ts +++ b/ts/src/program/namespace/index.ts @@ -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]; } } diff --git a/ts/src/program/namespace/instruction.ts b/ts/src/program/namespace/instruction.ts index a796fac71..92fc50182 100644 --- a/ts/src/program/namespace/instruction.ts +++ b/ts/src/program/namespace/instruction.ts @@ -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.(...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 diff --git a/ts/src/program/namespace/rpc.ts b/ts/src/program/namespace/rpc.ts index 10ff71836..8dfe805c6 100644 --- a/ts/src/program/namespace/rpc.ts +++ b/ts/src/program/namespace/rpc.ts @@ -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; +import { TransactionFn } from "./transaction"; export default class RpcFactory { - // Builds the rpc namespace. public static build( idlIx: IdlInstruction, - txFn: TxFn, + txFn: TransactionFn, idlErrors: Map, 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.(...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; diff --git a/ts/src/program/namespace/simulate.ts b/ts/src/program/namespace/simulate.ts index 48e1618a0..310ea9759 100644 --- a/ts/src/program/namespace/simulate.ts +++ b/ts/src/program/namespace/simulate.ts @@ -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; - -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, 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.(...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; + +type SimulateResponse = { + events: Event[]; + raw: string[]; +}; diff --git a/ts/src/program/namespace/state.ts b/ts/src/program/namespace/state.ts index dacc6db28..84039d706 100644 --- a/ts/src/program/namespace/state.ts +++ b/ts/src/program/namespace/state.ts @@ -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 - | { - address: () => Promise; - 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, 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 => { - 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 => { - 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 => { - 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 => { - 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 => - 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 { + 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 { - 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, }, diff --git a/ts/src/program/namespace/transaction.ts b/ts/src/program/namespace/transaction.ts index 06ee67aab..484f7e085 100644 --- a/ts/src/program/namespace/transaction.ts +++ b/ts/src/program/namespace/transaction.ts @@ -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.(...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; diff --git a/ts/src/utils/index.ts b/ts/src/utils/index.ts new file mode 100644 index 000000000..9d8ee8319 --- /dev/null +++ b/ts/src/utils/index.ts @@ -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 }; diff --git a/ts/src/utils/pubkey.ts b/ts/src/utils/pubkey.ts new file mode 100644 index 000000000..efa0527c7 --- /dev/null +++ b/ts/src/utils/pubkey.ts @@ -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, + 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, + 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): 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); + } +}; diff --git a/ts/src/utils.ts b/ts/src/utils/rpc.ts similarity index 72% rename from ts/src/utils.ts rename to ts/src/utils/rpc.ts index 8410b8fde..a6595e53a 100644 --- a/ts/src/utils.ts +++ b/ts/src/utils/rpc.ts @@ -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;