ts: Pubkeys as base58 strings (#304)

This commit is contained in:
Armani Ferrante 2021-05-23 09:58:15 -07:00 committed by GitHub
parent ccf18557f9
commit 8fa867fbd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 76 additions and 36 deletions

View File

@ -14,6 +14,7 @@ incremented for features.
## Features ## Features
* ts: Add `program.simulate` namespace ([#266](https://github.com/project-serum/anchor/pull/266)). * 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 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 `--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)). * 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)).

View File

@ -207,6 +207,8 @@ describe("misc", () => {
assert.ok(dataAccount.data === -3); assert.ok(dataAccount.data === -3);
}); });
let dataPubkey;
it("Can use i16 in the idl", async () => { it("Can use i16 in the idl", async () => {
const data = anchor.web3.Keypair.generate(); const data = anchor.web3.Keypair.generate();
await program.rpc.testI16(-2048, { await program.rpc.testI16(-2048, {
@ -219,5 +221,12 @@ describe("misc", () => {
}); });
const dataAccount = await program.account.dataI16(data.publicKey); const dataAccount = await program.account.dataI16(data.publicKey);
assert.ok(dataAccount.data === -2048); 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);
}); });
}); });

View File

@ -6,6 +6,7 @@ import { Idl } from "./idl";
import workspace from "./workspace"; import workspace from "./workspace";
import utils from "./utils"; import utils from "./utils";
import { Program } from "./program"; import { Program } from "./program";
import { Address } from "./program/common";
import { ProgramAccount } from "./program/namespace"; import { ProgramAccount } from "./program/namespace";
import { Context, Accounts } from "./program/context"; import { Context, Accounts } from "./program/context";
@ -37,4 +38,5 @@ export {
Idl, Idl,
utils, utils,
Wallet, Wallet,
Address,
}; };

View File

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

View File

@ -1,10 +1,10 @@
import { import {
AccountMeta, AccountMeta,
Signer, Signer,
PublicKey,
ConfirmOptions, ConfirmOptions,
TransactionInstruction, TransactionInstruction,
} from "@solana/web3.js"; } from "@solana/web3.js";
import { Address } from "./common";
import { IdlInstruction } from "../idl"; import { IdlInstruction } from "../idl";
/** /**
@ -56,7 +56,7 @@ export type Context = {
* nested here. * nested here.
*/ */
export type Accounts = { export type Accounts = {
[key: string]: PublicKey | Accounts; [key: string]: Address | Accounts;
}; };
export function splitArgsAndCtx( export function splitArgsAndCtx(

View File

@ -14,6 +14,7 @@ import NamespaceFactory, {
import { getProvider } from "../"; import { getProvider } from "../";
import { decodeUtf8 } from "../utils"; import { decodeUtf8 } from "../utils";
import { EventParser } from "./event"; import { EventParser } from "./event";
import { Address, translateAddress } from "./common";
/** /**
* ## Program * ## Program
@ -78,19 +79,19 @@ export class Program {
* ## account * ## account
* *
* ```javascript * ```javascript
* program.account.<account>(publicKey); * program.account.<account>(address);
* ``` * ```
* *
* ## Parameters * ## Parameters
* *
* 1. `publicKey` - The [[PublicKey]] of the account. * 1. `address` - The [[Address]] of the account.
* *
* ## Example * ## Example
* *
* To fetch a `Counter` object from the above example, * To fetch a `Counter` object from the above example,
* *
* ```javascript * ```javascript
* const counter = await program.account.counter(publicKey); * const counter = await program.account.counter(address);
* ``` * ```
*/ */
readonly account: AccountNamespace; readonly account: AccountNamespace;
@ -233,7 +234,9 @@ export class Program {
* @param provider The network and wallet context to use. If not provided * @param provider The network and wallet context to use. If not provided
* then uses [[getProvider]]. * then uses [[getProvider]].
*/ */
public constructor(idl: Idl, programId: PublicKey, provider?: Provider) { public constructor(idl: Idl, programId: Address, provider?: Provider) {
programId = translateAddress(programId);
// Fields. // Fields.
this._idl = idl; this._idl = idl;
this._programId = programId; this._programId = programId;
@ -266,7 +269,9 @@ export class Program {
* @param programId The on-chain address of the program. * @param programId The on-chain address of the program.
* @param provider The network and wallet context. * @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); const idl = await Program.fetchIdl(programId, provider);
return new Program(idl, 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 programId The on-chain address of the program.
* @param provider The network and wallet context. * @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(); provider = provider ?? getProvider();
const address = await idlAddress(programId); const programId = translateAddress(address);
const accountInfo = await provider.connection.getAccountInfo(address);
const idlAddr = await idlAddress(programId);
const accountInfo = await provider.connection.getAccountInfo(idlAddr);
// Chop off account discriminator. // Chop off account discriminator.
let idlAccount = decodeIdlAccount(accountInfo.data.slice(8)); let idlAccount = decodeIdlAccount(accountInfo.data.slice(8));
const inflatedIdl = inflate(idlAccount.data); const inflatedIdl = inflate(idlAccount.data);

View File

@ -15,7 +15,7 @@ import Coder, {
accountDiscriminator, accountDiscriminator,
accountSize, accountSize,
} from "../../coder"; } from "../../coder";
import { Subscription } from "../common"; import { Subscription, Address, translateAddress } from "../common";
/** /**
* Accounts is a dynamically generated object to fetch any given account * 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 = { type AccountProps = {
size: number; size: number;
all: (filter?: Buffer) => Promise<ProgramAccount<any>[]>; all: (filter?: Buffer) => Promise<ProgramAccount<any>[]>;
subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter; subscribe: (address: Address, commitment?: Commitment) => EventEmitter;
unsubscribe: (address: PublicKey) => void; unsubscribe: (address: Address) => void;
createInstruction: (signer: Signer) => Promise<TransactionInstruction>; createInstruction: (signer: Signer) => Promise<TransactionInstruction>;
associated: (...args: PublicKey[]) => Promise<any>; associated: (...args: PublicKey[]) => Promise<any>;
associatedAddress: (...args: PublicKey[]) => Promise<PublicKey>; associatedAddress: (...args: PublicKey[]) => Promise<PublicKey>;
@ -70,8 +70,10 @@ export default class AccountFactory {
const name = camelCase(idlAccount.name); const name = camelCase(idlAccount.name);
// Fetches the decoded account from the network. // Fetches the decoded account from the network.
const accountsNamespace = async (address: PublicKey): Promise<any> => { const accountsNamespace = async (address: Address): Promise<any> => {
const accountInfo = await provider.connection.getAccountInfo(address); const accountInfo = await provider.connection.getAccountInfo(
translateAddress(address)
);
if (accountInfo === null) { if (accountInfo === null) {
throw new Error(`Account does not exist ${address.toString()}`); throw new Error(`Account does not exist ${address.toString()}`);
} }
@ -113,14 +115,15 @@ export default class AccountFactory {
// Subscribes to all changes to this account. // Subscribes to all changes to this account.
// @ts-ignore // @ts-ignore
accountsNamespace["subscribe"] = ( accountsNamespace["subscribe"] = (
address: PublicKey, address: Address,
commitment?: Commitment commitment?: Commitment
): EventEmitter => { ): EventEmitter => {
if (subscriptions.get(address.toString())) { if (subscriptions.get(address.toString())) {
return subscriptions.get(address.toString()).ee; return subscriptions.get(address.toString()).ee;
} }
const ee = new EventEmitter();
const ee = new EventEmitter();
address = translateAddress(address);
const listener = provider.connection.onAccountChange( const listener = provider.connection.onAccountChange(
address, address,
(acc) => { (acc) => {
@ -140,7 +143,7 @@ export default class AccountFactory {
// Unsubscribes to account changes. // Unsubscribes to account changes.
// @ts-ignore // @ts-ignore
accountsNamespace["unsubscribe"] = (address: PublicKey) => { accountsNamespace["unsubscribe"] = (address: Address) => {
let sub = subscriptions.get(address.toString()); let sub = subscriptions.get(address.toString());
if (!sub) { if (!sub) {
console.warn("Address is not subscribed"); console.warn("Address is not subscribed");
@ -200,11 +203,11 @@ export default class AccountFactory {
// Function returning the associated address. Args are keys to associate. // Function returning the associated address. Args are keys to associate.
// Order matters. // Order matters.
accountsNamespace["associatedAddress"] = async ( accountsNamespace["associatedAddress"] = async (
...args: PublicKey[] ...args: Address[]
): Promise<PublicKey> => { ): Promise<PublicKey> => {
let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor". let seeds = [Buffer.from([97, 110, 99, 104, 111, 114])]; // b"anchor".
args.forEach((arg) => { args.forEach((arg) => {
seeds.push(arg.toBuffer()); seeds.push(translateAddress(arg).toBuffer());
}); });
const [assoc] = await PublicKey.findProgramAddress(seeds, programId); const [assoc] = await PublicKey.findProgramAddress(seeds, programId);
return assoc; return assoc;
@ -213,7 +216,7 @@ export default class AccountFactory {
// Function returning the associated account. Args are keys to associate. // Function returning the associated account. Args are keys to associate.
// Order matters. // Order matters.
accountsNamespace["associated"] = async ( accountsNamespace["associated"] = async (
...args: PublicKey[] ...args: Address[]
): Promise<any> => { ): Promise<any> => {
const addr = await accountsNamespace["associatedAddress"](...args); const addr = await accountsNamespace["associatedAddress"](...args);
return await accountsNamespace(addr); return await accountsNamespace(addr);

View File

@ -2,7 +2,12 @@ import { PublicKey, TransactionInstruction } from "@solana/web3.js";
import { IdlAccount, IdlInstruction, IdlAccountItem } from "../../idl"; import { IdlAccount, IdlInstruction, IdlAccountItem } from "../../idl";
import { IdlError } from "../../error"; import { IdlError } from "../../error";
import Coder from "../../coder"; import Coder from "../../coder";
import { toInstruction, validateAccounts } from "../common"; import {
toInstruction,
validateAccounts,
translateAddress,
Address,
} from "../common";
import { Accounts, splitArgsAndCtx } from "../context"; import { Accounts, splitArgsAndCtx } from "../context";
/** /**
@ -81,7 +86,7 @@ export default class InstructionNamespaceFactory {
} else { } else {
const account: IdlAccount = acc as IdlAccount; const account: IdlAccount = acc as IdlAccount;
return { return {
pubkey: ctx[acc.name], pubkey: translateAddress(ctx[acc.name] as Address),
isWritable: account.isMut, isWritable: account.isMut,
isSigner: account.isSigner, isSigner: account.isSigner,
}; };

View File

@ -27,7 +27,7 @@ export default class RpcFactory {
): RpcFn { ): RpcFn {
const rpc = async (...args: any[]): Promise<TransactionSignature> => { const rpc = async (...args: any[]): Promise<TransactionSignature> => {
const tx = txFn(...args); const tx = txFn(...args);
const [_, ctx] = splitArgsAndCtx(idlIx, [...args]); const [, ctx] = splitArgsAndCtx(idlIx, [...args]);
try { try {
const txSig = await provider.send(tx, ctx.signers, ctx.options); const txSig = await provider.send(tx, ctx.signers, ctx.options);
return txSig; return txSig;

View File

@ -27,8 +27,8 @@ export type StateNamespace = () =>
address: () => Promise<PublicKey>; address: () => Promise<PublicKey>;
rpc: RpcNamespace; rpc: RpcNamespace;
instruction: InstructionNamespace; instruction: InstructionNamespace;
subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter; subscribe: (commitment?: Commitment) => EventEmitter;
unsubscribe: (address: PublicKey) => void; unsubscribe: () => void;
}; };
export default class StateFactory { export default class StateFactory {
@ -92,7 +92,7 @@ export default class StateFactory {
ix[m.name] = ixFn; ix[m.name] = ixFn;
rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => { rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
const [_, ctx] = splitArgsAndCtx(m, [...args]); const [, ctx] = splitArgsAndCtx(m, [...args]);
const tx = new Transaction(); const tx = new Transaction();
if (ctx.instructions !== undefined) { if (ctx.instructions !== undefined) {
tx.add(...ctx.instructions); tx.add(...ctx.instructions);
@ -164,10 +164,7 @@ export default class StateFactory {
// Calculates the deterministic address of the program's "state" account. // Calculates the deterministic address of the program's "state" account.
async function programStateAddress(programId: PublicKey): Promise<PublicKey> { async function programStateAddress(programId: PublicKey): Promise<PublicKey> {
let [registrySigner, _nonce] = await PublicKey.findProgramAddress( let [registrySigner] = await PublicKey.findProgramAddress([], programId);
[],
programId
);
return PublicKey.createWithSeed(registrySigner, "unversioned", programId); return PublicKey.createWithSeed(registrySigner, "unversioned", programId);
} }
@ -181,10 +178,7 @@ async function stateInstructionKeys(
) { ) {
if (m.name === "new") { if (m.name === "new") {
// Ctor `new` method. // Ctor `new` method.
const [programSigner, _nonce] = await PublicKey.findProgramAddress( const [programSigner] = await PublicKey.findProgramAddress([], programId);
[],
programId
);
return [ return [
{ {
pubkey: provider.wallet.publicKey, pubkey: provider.wallet.publicKey,

View File

@ -19,7 +19,7 @@ export default class TransactionFactory {
// Builds the transaction namespace. // Builds the transaction namespace.
public static build(idlIx: IdlInstruction, ixFn: IxFn): TxFn { public static build(idlIx: IdlInstruction, ixFn: IxFn): TxFn {
const txFn = (...args: any[]): Transaction => { const txFn = (...args: any[]): Transaction => {
const [_, ctx] = splitArgsAndCtx(idlIx, [...args]); const [, ctx] = splitArgsAndCtx(idlIx, [...args]);
const tx = new Transaction(); const tx = new Transaction();
if (ctx.instructions !== undefined) { if (ctx.instructions !== undefined) {
tx.add(...ctx.instructions); tx.add(...ctx.instructions);