From 8fa867fbd695722b4cc592efd6f7ce462fc18f9b Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Sun, 23 May 2021 09:58:15 -0700 Subject: [PATCH] ts: Pubkeys as base58 strings (#304) --- CHANGELOG.md | 1 + examples/misc/tests/misc.js | 9 +++++++++ ts/src/index.ts | 2 ++ ts/src/program/common.ts | 19 +++++++++++++++++++ ts/src/program/context.ts | 4 ++-- ts/src/program/index.ts | 23 +++++++++++++++-------- ts/src/program/namespace/account.ts | 25 ++++++++++++++----------- ts/src/program/namespace/instruction.ts | 9 +++++++-- ts/src/program/namespace/rpc.ts | 2 +- ts/src/program/namespace/state.ts | 16 +++++----------- ts/src/program/namespace/transaction.ts | 2 +- 11 files changed, 76 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d308b46d..2a80314d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ incremented for features. ## Features * ts: Add `program.simulate` namespace ([#266](https://github.com/project-serum/anchor/pull/266)). +* ts: Introduce `Address` type, allowing one to use Base 58 encoded strings in public APIs ([#304](https://github.com/project-serum/anchor/pull/304)). * cli: Add yarn flag to test command ([#267](https://github.com/project-serum/anchor/pull/267)). * cli: Add `--skip-build` flag to test command ([301](https://github.com/project-serum/anchor/pull/301)). * cli: Add `anchor shell` command to spawn a node shell populated with an Anchor.toml based environment ([#303](https://github.com/project-serum/anchor/pull/303)). diff --git a/examples/misc/tests/misc.js b/examples/misc/tests/misc.js index f8473807..61043f10 100644 --- a/examples/misc/tests/misc.js +++ b/examples/misc/tests/misc.js @@ -207,6 +207,8 @@ describe("misc", () => { assert.ok(dataAccount.data === -3); }); + let dataPubkey; + it("Can use i16 in the idl", async () => { const data = anchor.web3.Keypair.generate(); await program.rpc.testI16(-2048, { @@ -219,5 +221,12 @@ describe("misc", () => { }); const dataAccount = await program.account.dataI16(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()); + assert.ok(dataAccount.data === -2048); }); }); diff --git a/ts/src/index.ts b/ts/src/index.ts index 3385c920..964aa737 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -6,6 +6,7 @@ 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 { Context, Accounts } from "./program/context"; @@ -37,4 +38,5 @@ export { Idl, utils, Wallet, + Address, }; diff --git a/ts/src/program/common.ts b/ts/src/program/common.ts index 312134fb..8e902f51 100644 --- a/ts/src/program/common.ts +++ b/ts/src/program/common.ts @@ -1,7 +1,10 @@ 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; @@ -77,3 +80,19 @@ export function translateError( } } } + +// Translates an address to a Pubkey. +export function translateAddress(address: Address): PublicKey { + if (typeof address === "string") { + const pk = new PublicKey(address); + return pk; + } else { + return address; + } +} + +/** + * An address to identify an account on chain. Can be a [[PublicKey]], + * or Base 58 encoded string. + */ +export type Address = PublicKey | string; diff --git a/ts/src/program/context.ts b/ts/src/program/context.ts index e11dc95c..721f413e 100644 --- a/ts/src/program/context.ts +++ b/ts/src/program/context.ts @@ -1,10 +1,10 @@ import { AccountMeta, Signer, - PublicKey, ConfirmOptions, TransactionInstruction, } from "@solana/web3.js"; +import { Address } from "./common"; import { IdlInstruction } from "../idl"; /** @@ -56,7 +56,7 @@ export type Context = { * nested here. */ export type Accounts = { - [key: string]: PublicKey | Accounts; + [key: string]: Address | Accounts; }; export function splitArgsAndCtx( diff --git a/ts/src/program/index.ts b/ts/src/program/index.ts index 711131a0..99f15f62 100644 --- a/ts/src/program/index.ts +++ b/ts/src/program/index.ts @@ -14,6 +14,7 @@ import NamespaceFactory, { import { getProvider } from "../"; import { decodeUtf8 } from "../utils"; import { EventParser } from "./event"; +import { Address, translateAddress } from "./common"; /** * ## Program @@ -78,19 +79,19 @@ export class Program { * ## account * * ```javascript - * program.account.(publicKey); + * program.account.(address); * ``` * * ## Parameters * - * 1. `publicKey` - The [[PublicKey]] of the account. + * 1. `address` - The [[Address]] of the account. * * ## Example * * To fetch a `Counter` object from the above example, * * ```javascript - * const counter = await program.account.counter(publicKey); + * const counter = await program.account.counter(address); * ``` */ readonly account: AccountNamespace; @@ -233,7 +234,9 @@ export class Program { * @param provider The network and wallet context to use. If not provided * then uses [[getProvider]]. */ - public constructor(idl: Idl, programId: PublicKey, provider?: Provider) { + public constructor(idl: Idl, programId: Address, provider?: Provider) { + programId = translateAddress(programId); + // Fields. this._idl = idl; this._programId = programId; @@ -266,7 +269,9 @@ export class Program { * @param programId The on-chain address of the program. * @param provider The network and wallet context. */ - public static async at(programId: PublicKey, provider?: Provider) { + public static async at(address: Address, provider?: Provider) { + const programId = translateAddress(address); + const idl = await Program.fetchIdl(programId, provider); return new Program(idl, programId, provider); } @@ -280,10 +285,12 @@ export class Program { * @param programId The on-chain address of the program. * @param provider The network and wallet context. */ - public static async fetchIdl(programId: PublicKey, provider?: Provider) { + public static async fetchIdl(address: Address, provider?: Provider) { provider = provider ?? getProvider(); - const address = await idlAddress(programId); - const accountInfo = await provider.connection.getAccountInfo(address); + const programId = translateAddress(address); + + const idlAddr = await idlAddress(programId); + const accountInfo = await provider.connection.getAccountInfo(idlAddr); // Chop off account discriminator. let idlAccount = decodeIdlAccount(accountInfo.data.slice(8)); const inflatedIdl = inflate(idlAccount.data); diff --git a/ts/src/program/namespace/account.ts b/ts/src/program/namespace/account.ts index cf032fcd..cc3aa2b6 100644 --- a/ts/src/program/namespace/account.ts +++ b/ts/src/program/namespace/account.ts @@ -15,7 +15,7 @@ import Coder, { accountDiscriminator, accountSize, } from "../../coder"; -import { Subscription } from "../common"; +import { Subscription, Address, translateAddress } from "../common"; /** * Accounts is a dynamically generated object to fetch any given account @@ -36,8 +36,8 @@ export type AccountFn = AccountProps & ((address: PublicKey) => T); type AccountProps = { size: number; all: (filter?: Buffer) => Promise[]>; - subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter; - unsubscribe: (address: PublicKey) => void; + subscribe: (address: Address, commitment?: Commitment) => EventEmitter; + unsubscribe: (address: Address) => void; createInstruction: (signer: Signer) => Promise; associated: (...args: PublicKey[]) => Promise; associatedAddress: (...args: PublicKey[]) => Promise; @@ -70,8 +70,10 @@ export default class AccountFactory { const name = camelCase(idlAccount.name); // Fetches the decoded account from the network. - const accountsNamespace = async (address: PublicKey): Promise => { - const accountInfo = await provider.connection.getAccountInfo(address); + 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()}`); } @@ -113,14 +115,15 @@ export default class AccountFactory { // Subscribes to all changes to this account. // @ts-ignore accountsNamespace["subscribe"] = ( - address: PublicKey, + address: Address, commitment?: Commitment ): EventEmitter => { if (subscriptions.get(address.toString())) { return subscriptions.get(address.toString()).ee; } - const ee = new EventEmitter(); + const ee = new EventEmitter(); + address = translateAddress(address); const listener = provider.connection.onAccountChange( address, (acc) => { @@ -140,7 +143,7 @@ export default class AccountFactory { // Unsubscribes to account changes. // @ts-ignore - accountsNamespace["unsubscribe"] = (address: PublicKey) => { + accountsNamespace["unsubscribe"] = (address: Address) => { let sub = subscriptions.get(address.toString()); if (!sub) { console.warn("Address is not subscribed"); @@ -200,11 +203,11 @@ export default class AccountFactory { // Function returning the associated address. Args are keys to associate. // Order matters. accountsNamespace["associatedAddress"] = async ( - ...args: PublicKey[] + ...args: Address[] ): Promise => { let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor". args.forEach((arg) => { - seeds.push(arg.toBuffer()); + seeds.push(translateAddress(arg).toBuffer()); }); const [assoc] = await PublicKey.findProgramAddress(seeds, programId); return assoc; @@ -213,7 +216,7 @@ export default class AccountFactory { // Function returning the associated account. Args are keys to associate. // Order matters. accountsNamespace["associated"] = async ( - ...args: PublicKey[] + ...args: Address[] ): Promise => { const addr = await accountsNamespace["associatedAddress"](...args); return await accountsNamespace(addr); diff --git a/ts/src/program/namespace/instruction.ts b/ts/src/program/namespace/instruction.ts index 06543adc..a796fac7 100644 --- a/ts/src/program/namespace/instruction.ts +++ b/ts/src/program/namespace/instruction.ts @@ -2,7 +2,12 @@ import { PublicKey, TransactionInstruction } from "@solana/web3.js"; import { IdlAccount, IdlInstruction, IdlAccountItem } from "../../idl"; import { IdlError } from "../../error"; import Coder from "../../coder"; -import { toInstruction, validateAccounts } from "../common"; +import { + toInstruction, + validateAccounts, + translateAddress, + Address, +} from "../common"; import { Accounts, splitArgsAndCtx } from "../context"; /** @@ -81,7 +86,7 @@ export default class InstructionNamespaceFactory { } else { const account: IdlAccount = acc as IdlAccount; return { - pubkey: ctx[acc.name], + pubkey: translateAddress(ctx[acc.name] as Address), isWritable: account.isMut, isSigner: account.isSigner, }; diff --git a/ts/src/program/namespace/rpc.ts b/ts/src/program/namespace/rpc.ts index bd82381e..10ff7183 100644 --- a/ts/src/program/namespace/rpc.ts +++ b/ts/src/program/namespace/rpc.ts @@ -27,7 +27,7 @@ export default class RpcFactory { ): RpcFn { const rpc = async (...args: any[]): Promise => { const tx = txFn(...args); - const [_, ctx] = splitArgsAndCtx(idlIx, [...args]); + const [, ctx] = splitArgsAndCtx(idlIx, [...args]); try { const txSig = await provider.send(tx, ctx.signers, ctx.options); return txSig; diff --git a/ts/src/program/namespace/state.ts b/ts/src/program/namespace/state.ts index 61a68b32..dacc6db2 100644 --- a/ts/src/program/namespace/state.ts +++ b/ts/src/program/namespace/state.ts @@ -27,8 +27,8 @@ export type StateNamespace = () => address: () => Promise; rpc: RpcNamespace; instruction: InstructionNamespace; - subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter; - unsubscribe: (address: PublicKey) => void; + subscribe: (commitment?: Commitment) => EventEmitter; + unsubscribe: () => void; }; export default class StateFactory { @@ -92,7 +92,7 @@ export default class StateFactory { ix[m.name] = ixFn; rpc[m.name] = async (...args: any[]): Promise => { - const [_, ctx] = splitArgsAndCtx(m, [...args]); + const [, ctx] = splitArgsAndCtx(m, [...args]); const tx = new Transaction(); if (ctx.instructions !== undefined) { tx.add(...ctx.instructions); @@ -164,10 +164,7 @@ export default class StateFactory { // Calculates the deterministic address of the program's "state" account. async function programStateAddress(programId: PublicKey): Promise { - let [registrySigner, _nonce] = await PublicKey.findProgramAddress( - [], - programId - ); + let [registrySigner] = await PublicKey.findProgramAddress([], programId); return PublicKey.createWithSeed(registrySigner, "unversioned", programId); } @@ -181,10 +178,7 @@ async function stateInstructionKeys( ) { if (m.name === "new") { // Ctor `new` method. - const [programSigner, _nonce] = await PublicKey.findProgramAddress( - [], - programId - ); + const [programSigner] = await PublicKey.findProgramAddress([], programId); return [ { pubkey: provider.wallet.publicKey, diff --git a/ts/src/program/namespace/transaction.ts b/ts/src/program/namespace/transaction.ts index 30cabf83..06ee67aa 100644 --- a/ts/src/program/namespace/transaction.ts +++ b/ts/src/program/namespace/transaction.ts @@ -19,7 +19,7 @@ export default class TransactionFactory { // Builds the transaction namespace. public static build(idlIx: IdlInstruction, ixFn: IxFn): TxFn { const txFn = (...args: any[]): Transaction => { - const [_, ctx] = splitArgsAndCtx(idlIx, [...args]); + const [, ctx] = splitArgsAndCtx(idlIx, [...args]); const tx = new Transaction(); if (ctx.instructions !== undefined) { tx.add(...ctx.instructions);