Browse Source

ts: Pubkeys as base58 strings (#304)

Armani Ferrante 4 years ago
parent
commit
8fa867fbd6

+ 1 - 0
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)).

+ 9 - 0
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);
   });
 });

+ 2 - 0
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,
 };

+ 19 - 0
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;

+ 2 - 2
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(

+ 15 - 8
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.<account>(publicKey);
+   * program.account.<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);

+ 14 - 11
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<T = any> = AccountProps & ((address: PublicKey) => T);
 type AccountProps = {
   size: number;
   all: (filter?: Buffer) => Promise<ProgramAccount<any>[]>;
-  subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter;
-  unsubscribe: (address: PublicKey) => void;
+  subscribe: (address: Address, commitment?: Commitment) => EventEmitter;
+  unsubscribe: (address: Address) => void;
   createInstruction: (signer: Signer) => Promise<TransactionInstruction>;
   associated: (...args: PublicKey[]) => Promise<any>;
   associatedAddress: (...args: PublicKey[]) => Promise<PublicKey>;
@@ -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<any> => {
-        const accountInfo = await provider.connection.getAccountInfo(address);
+      const accountsNamespace = async (address: Address): Promise<any> => {
+        const accountInfo = await provider.connection.getAccountInfo(
+          translateAddress(address)
+        );
         if (accountInfo === null) {
           throw new Error(`Account does not exist ${address.toString()}`);
         }
@@ -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<PublicKey> => {
         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<any> => {
         const addr = await accountsNamespace["associatedAddress"](...args);
         return await accountsNamespace(addr);

+ 7 - 2
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,
           };

+ 1 - 1
ts/src/program/namespace/rpc.ts

@@ -27,7 +27,7 @@ export default class RpcFactory {
   ): RpcFn {
     const rpc = async (...args: any[]): Promise<TransactionSignature> => {
       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;

+ 5 - 11
ts/src/program/namespace/state.ts

@@ -27,8 +27,8 @@ export type StateNamespace = () =>
       address: () => Promise<PublicKey>;
       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<TransactionSignature> => {
-        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<PublicKey> {
-  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,

+ 1 - 1
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);