feat: add parsed account data APIs

This commit is contained in:
Justin Starry 2020-08-06 23:47:22 +08:00 committed by Justin Starry
parent b36e60738e
commit c7a2fbe7eb
5 changed files with 452 additions and 59 deletions

View File

@ -1,6 +1,7 @@
declare module 'superstruct' {
declare type StructFunc = {
(any): any,
object(schema: any): any;
union(schema: any): any;
array(schema: any): any;
literal(schema: any): any;

46
web3.js/module.d.ts vendored
View File

@ -104,16 +104,16 @@ declare module '@solana/web3.js' {
feeCalculator: FeeCalculator;
};
export type PublicKeyAndAccount = {
export type PublicKeyAndAccount<T> = {
pubkey: PublicKey;
account: AccountInfo;
account: AccountInfo<T>;
};
export type AccountInfo = {
export type AccountInfo<T> = {
executable: boolean;
owner: PublicKey;
lamports: number;
data: Buffer;
data: T;
rentEpoch?: number;
};
@ -181,9 +181,14 @@ declare module '@solana/web3.js' {
meta: ConfirmedTransactionMeta | null;
};
export type ParsedAccountData = {
program: string;
parsed: any;
};
export type KeyedAccountInfo = {
accountId: PublicKey;
accountInfo: AccountInfo;
accountInfo: AccountInfo<Buffer>;
};
export type Version = {
@ -210,7 +215,7 @@ declare module '@solana/web3.js' {
};
export type AccountChangeCallback = (
accountInfo: AccountInfo,
accountInfo: AccountInfo<Buffer>,
context: Context,
) => void;
export type ProgramAccountChangeCallback = (
@ -280,15 +285,25 @@ declare module '@solana/web3.js' {
getAccountInfoAndContext(
publicKey: PublicKey,
commitment?: Commitment,
): Promise<RpcResponseAndContext<AccountInfo | null>>;
): Promise<RpcResponseAndContext<AccountInfo<Buffer> | null>>;
getAccountInfo(
publicKey: PublicKey,
commitment?: Commitment,
): Promise<AccountInfo | null>;
): Promise<AccountInfo<Buffer> | null>;
getParsedAccountInfo(
publicKey: PublicKey,
commitment?: Commitment,
): Promise<
RpcResponseAndContext<AccountInfo<Buffer | ParsedAccountData> | null>
>;
getProgramAccounts(
programId: PublicKey,
commitment?: Commitment,
): Promise<Array<PublicKeyAndAccount>>;
): Promise<Array<PublicKeyAndAccount<Buffer>>>;
getParsedProgramAccounts(
programId: PublicKey,
commitment?: Commitment,
): Promise<Array<PublicKeyAndAccount<Buffer | ParsedAccountData>>>;
getBalanceAndContext(
publicKey: PublicKey,
commitment?: Commitment,
@ -311,7 +326,18 @@ declare module '@solana/web3.js' {
filter: TokenAccountsFilter,
commitment?: Commitment,
): Promise<
RpcResponseAndContext<Array<{pubkey: PublicKey; account: AccountInfo}>>
RpcResponseAndContext<
Array<{pubkey: PublicKey; account: AccountInfo<Buffer>}>
>
>;
getParsedTokenAccountsByOwner(
ownerAddress: PublicKey,
filter: TokenAccountsFilter,
commitment?: Commitment,
): Promise<
RpcResponseAndContext<
Array<{pubkey: PublicKey; account: AccountInfo<ParsedAccountData>}>
>
>;
getLargestAccounts(
config?: GetLargestAccountsConfig,

View File

@ -125,16 +125,16 @@ declare module '@solana/web3.js' {
feeCalculator: FeeCalculator,
};
declare export type PublicKeyAndAccount = {
declare export type PublicKeyAndAccount<T> = {
pubkey: PublicKey,
account: AccountInfo,
account: AccountInfo<T>,
};
declare export type AccountInfo = {
declare export type AccountInfo<T> = {
executable: boolean,
owner: PublicKey,
lamports: number,
data: Buffer,
data: T,
rentEpoch: number | null,
};
@ -169,6 +169,11 @@ declare module '@solana/web3.js' {
meta: ConfirmedTransactionMeta | null,
};
declare export type ParsedAccountData = {
program: string,
parsed: any,
};
declare export type ParsedMessageAccount = {
pubkey: PublicKey,
signer: boolean,
@ -204,7 +209,7 @@ declare module '@solana/web3.js' {
declare export type KeyedAccountInfo = {
accountId: PublicKey,
accountInfo: AccountInfo,
accountInfo: AccountInfo<Buffer>,
};
declare export type Version = {
@ -231,7 +236,7 @@ declare module '@solana/web3.js' {
};
declare type AccountChangeCallback = (
accountInfo: AccountInfo,
accountInfo: AccountInfo<Buffer>,
context: Context,
) => void;
declare type ProgramAccountChangeCallback = (
@ -301,15 +306,25 @@ declare module '@solana/web3.js' {
getAccountInfoAndContext(
publicKey: PublicKey,
commitment: ?Commitment,
): Promise<RpcResponseAndContext<AccountInfo | null>>;
): Promise<RpcResponseAndContext<AccountInfo<Buffer> | null>>;
getAccountInfo(
publicKey: PublicKey,
commitment: ?Commitment,
): Promise<AccountInfo | null>;
): Promise<AccountInfo<Buffer> | null>;
getParsedAccountInfo(
publicKey: PublicKey,
commitment: ?Commitment,
): Promise<
RpcResponseAndContext<AccountInfo<Buffer | ParsedAccountData> | null>,
>;
getProgramAccounts(
programId: PublicKey,
commitment: ?Commitment,
): Promise<Array<PublicKeyAndAccount>>;
): Promise<Array<PublicKeyAndAccount<Buffer>>>;
getParsedProgramAccounts(
programId: PublicKey,
commitment: ?Commitment,
): Promise<Array<PublicKeyAndAccount<Buffer | ParsedAccountData>>>;
getBalanceAndContext(
publicKey: PublicKey,
commitment: ?Commitment,
@ -332,7 +347,9 @@ declare module '@solana/web3.js' {
filter: TokenAccountsFilter,
commitment: ?Commitment,
): Promise<
RpcResponseAndContext<Array<{pubkey: PublicKey, account: AccountInfo}>>,
RpcResponseAndContext<
Array<{pubkey: PublicKey, account: AccountInfo<Buffer>}>,
>,
>;
getLargestAccounts(
config: ?GetLargestAccountsConfig,

View File

@ -25,12 +25,12 @@ export const BLOCKHASH_CACHE_TIMEOUT_MS = 30 * 1000;
type RpcRequest = (methodName: string, args: Array<any>) => any;
type TokenAccountsFilter =
| {
| {|
mint: PublicKey,
}
| {
|}
| {|
programId: PublicKey,
};
|};
/**
* Extra contextual information for RPC responses
@ -634,13 +634,34 @@ const GetTokenSupplyRpcResult = jsonRpcResultAndContext(TokenAmountResult);
*/
const GetTokenAccountsByOwner = jsonRpcResultAndContext(
struct.array([
struct({
struct.object({
pubkey: 'string',
account: struct({
account: struct.object({
executable: 'boolean',
owner: 'string',
lamports: 'number',
data: 'any',
data: 'string',
rentEpoch: 'number?',
}),
}),
]),
);
/**
* Expected JSON RPC response for the "getTokenAccountsByOwner" message with parsed data
*/
const GetParsedTokenAccountsByOwner = jsonRpcResultAndContext(
struct.array([
struct.object({
pubkey: 'string',
account: struct.object({
executable: 'boolean',
owner: 'string',
lamports: 'number',
data: struct.object({
program: 'string',
parsed: 'any',
}),
rentEpoch: 'number?',
}),
}),
@ -692,6 +713,23 @@ const AccountInfoResult = struct({
rentEpoch: 'number?',
});
/**
* @private
*/
const ParsedAccountInfoResult = struct.object({
executable: 'boolean',
owner: 'string',
lamports: 'number',
data: struct.union([
'string',
struct.object({
program: 'string',
parsed: 'any',
}),
]),
rentEpoch: 'number?',
});
/**
* Expected JSON RPC response for the "getAccountInfo" message
*/
@ -699,6 +737,13 @@ const GetAccountInfoAndContextRpcResult = jsonRpcResultAndContext(
struct.union(['null', AccountInfoResult]),
);
/**
* Expected JSON RPC response for the "getAccountInfo" message with jsonParsed param
*/
const GetParsedAccountInfoResult = jsonRpcResultAndContext(
struct.union(['null', ParsedAccountInfoResult]),
);
/**
* Expected JSON RPC response for the "getConfirmedSignaturesForAddress" message
*/
@ -737,6 +782,14 @@ const ProgramAccountInfoResult = struct({
account: AccountInfoResult,
});
/**
* @private
*/
const ParsedProgramAccountInfoResult = struct({
pubkey: 'string',
account: ParsedAccountInfoResult,
});
/***
* Expected JSON RPC response for the "programNotification" message
*/
@ -785,6 +838,13 @@ const GetProgramAccountsRpcResult = jsonRpcResult(
struct.array([ProgramAccountInfoResult]),
);
/**
* Expected JSON RPC response for the "getProgramAccounts" message
*/
const GetParsedProgramAccountsRpcResult = jsonRpcResult(
struct.array([ParsedProgramAccountInfoResult]),
);
/**
* Expected JSON RPC response for the "getSlot" message
*/
@ -1051,20 +1111,32 @@ type SlotInfo = {
root: number,
};
/**
* Parsed account data
*
* @typedef {Object} ParsedAccountData
* @property {string} program Name of the program that owns this account
* @property {any} parsed Parsed account data
*/
type ParsedAccountData = {
program: string,
parsed: any,
};
/**
* Information describing an account
*
* @typedef {Object} AccountInfo
* @property {number} lamports Number of lamports assigned to the account
* @property {PublicKey} owner Identifier of the program that owns the account
* @property {?Buffer} data Optional data assigned to the account
* @property {T} data Optional data assigned to the account
* @property {boolean} executable `true` if this account's data contains a loaded program
*/
type AccountInfo = {
type AccountInfo<T> = {
executable: boolean,
owner: PublicKey,
lamports: number,
data: Buffer,
data: T,
};
/**
@ -1072,18 +1144,18 @@ type AccountInfo = {
*
* @typedef {Object} KeyedAccountInfo
* @property {PublicKey} accountId
* @property {AccountInfo} accountInfo
* @property {AccountInfo<Buffer>} accountInfo
*/
type KeyedAccountInfo = {
accountId: PublicKey,
accountInfo: AccountInfo,
accountInfo: AccountInfo<Buffer>,
};
/**
* Callback function for account change notifications
*/
export type AccountChangeCallback = (
accountInfo: AccountInfo,
accountInfo: AccountInfo<Buffer>,
context: Context,
) => void;
@ -1450,25 +1522,23 @@ export class Connection {
/**
* Fetch all the token accounts owned by the specified account
*
* @return {Promise<RpcResponseAndContext<Array<{pubkey: PublicKey, account: AccountInfo}>>>}
* @return {Promise<RpcResponseAndContext<Array<{pubkey: PublicKey, account: AccountInfo<Buffer>}>>>}
*/
async getTokenAccountsByOwner(
ownerAddress: PublicKey,
filter: TokenAccountsFilter,
commitment: ?Commitment,
): Promise<
RpcResponseAndContext<Array<{pubkey: PublicKey, account: AccountInfo}>>,
RpcResponseAndContext<
Array<{pubkey: PublicKey, account: AccountInfo<Buffer>}>,
>,
> {
let _args = [ownerAddress.toBase58()];
// Strip flow types to make flow happy
((filter: any) => {
if ('mint' in filter) {
_args.push({mint: filter.mint.toBase58()});
} else {
_args.push({programId: filter.programId.toBase58()});
}
})(filter);
if (filter.mint) {
_args.push({mint: filter.mint.toBase58()});
} else {
_args.push({programId: filter.programId.toBase58()});
}
const args = this._argsWithCommitment(_args, commitment);
const unsafeRes = await this._rpcRequest('getTokenAccountsByOwner', args);
@ -1489,7 +1559,7 @@ export class Connection {
return {
context,
value: value.map(result => ({
pubkey: result.pubkey,
pubkey: new PublicKey(result.pubkey),
account: {
executable: result.account.executable,
owner: new PublicKey(result.account.owner),
@ -1500,6 +1570,57 @@ export class Connection {
};
}
/**
* Fetch parsed token accounts owned by the specified account
*
* @return {Promise<RpcResponseAndContext<Array<{pubkey: PublicKey, account: AccountInfo<ParsedAccountData>}>>>}
*/
async getParsedTokenAccountsByOwner(
ownerAddress: PublicKey,
filter: TokenAccountsFilter,
commitment: ?Commitment,
): Promise<
RpcResponseAndContext<
Array<{pubkey: PublicKey, account: AccountInfo<ParsedAccountData>}>,
>,
> {
let _args = [ownerAddress.toBase58()];
if (filter.mint) {
_args.push({mint: filter.mint.toBase58()});
} else {
_args.push({programId: filter.programId.toBase58()});
}
const args = this._argsWithCommitment(_args, commitment, 'jsonParsed');
const unsafeRes = await this._rpcRequest('getTokenAccountsByOwner', args);
const res = GetParsedTokenAccountsByOwner(unsafeRes);
if (res.error) {
throw new Error(
'failed to get token accounts owned by account ' +
ownerAddress.toBase58() +
': ' +
res.error.message,
);
}
const {result} = res;
const {context, value} = result;
assert(typeof result !== 'undefined');
return {
context,
value: value.map(result => ({
pubkey: new PublicKey(result.pubkey),
account: {
executable: result.account.executable,
owner: new PublicKey(result.account.owner),
lamports: result.account.lamports,
data: result.account.data,
},
})),
};
}
/**
* Fetch the 20 largest accounts with their current balances
*/
@ -1530,7 +1651,7 @@ export class Connection {
async getAccountInfoAndContext(
publicKey: PublicKey,
commitment: ?Commitment,
): Promise<RpcResponseAndContext<AccountInfo | null>> {
): Promise<RpcResponseAndContext<AccountInfo<Buffer> | null>> {
const args = this._argsWithCommitment([publicKey.toBase58()], commitment);
const unsafeRes = await this._rpcRequest('getAccountInfo', args);
const res = GetAccountInfoAndContextRpcResult(unsafeRes);
@ -1563,13 +1684,64 @@ export class Connection {
};
}
/**
* Fetch parsed account info for the specified public key
*/
async getParsedAccountInfo(
publicKey: PublicKey,
commitment: ?Commitment,
): Promise<
RpcResponseAndContext<AccountInfo<Buffer | ParsedAccountData> | null>,
> {
const args = this._argsWithCommitment(
[publicKey.toBase58()],
commitment,
'jsonParsed',
);
const unsafeRes = await this._rpcRequest('getAccountInfo', args);
const res = GetParsedAccountInfoResult(unsafeRes);
if (res.error) {
throw new Error(
'failed to get info about account ' +
publicKey.toBase58() +
': ' +
res.error.message,
);
}
assert(typeof res.result !== 'undefined');
let value = null;
if (res.result.value) {
const {executable, owner, lamports, data: resultData} = res.result.value;
let data = resultData;
if (!data.program) {
data = bs58.decode(data);
}
value = {
executable,
owner: new PublicKey(owner),
lamports,
data,
};
}
return {
context: {
slot: res.result.context.slot,
},
value,
};
}
/**
* Fetch all the account info for the specified public key
*/
async getAccountInfo(
publicKey: PublicKey,
commitment: ?Commitment,
): Promise<AccountInfo | null> {
): Promise<AccountInfo<Buffer> | null> {
return await this.getAccountInfoAndContext(publicKey, commitment)
.then(x => x.value)
.catch(e => {
@ -1582,12 +1754,12 @@ export class Connection {
/**
* Fetch all the accounts owned by the specified program id
*
* @return {Promise<Array<{pubkey: PublicKey, account: AccountInfo}>>}
* @return {Promise<Array<{pubkey: PublicKey, account: AccountInfo<Buffer>}>>}
*/
async getProgramAccounts(
programId: PublicKey,
commitment: ?Commitment,
): Promise<Array<{pubkey: PublicKey, account: AccountInfo}>> {
): Promise<Array<{pubkey: PublicKey, account: AccountInfo<Buffer>}>> {
const args = this._argsWithCommitment([programId.toBase58()], commitment);
const unsafeRes = await this._rpcRequest('getProgramAccounts', args);
const res = GetProgramAccountsRpcResult(unsafeRes);
@ -1605,7 +1777,7 @@ export class Connection {
return result.map(result => {
return {
pubkey: result.pubkey,
pubkey: new PublicKey(result.pubkey),
account: {
executable: result.account.executable,
owner: new PublicKey(result.account.owner),
@ -1616,6 +1788,59 @@ export class Connection {
});
}
/**
* Fetch and parse all the accounts owned by the specified program id
*
* @return {Promise<Array<{pubkey: PublicKey, account: AccountInfo<Buffer | ParsedAccountData>}>>}
*/
async getParsedProgramAccounts(
programId: PublicKey,
commitment: ?Commitment,
): Promise<
Array<{
pubkey: PublicKey,
account: AccountInfo<Buffer | ParsedAccountData>,
}>,
> {
const args = this._argsWithCommitment(
[programId.toBase58()],
commitment,
'jsonParsed',
);
const unsafeRes = await this._rpcRequest('getProgramAccounts', args);
const res = GetParsedProgramAccountsRpcResult(unsafeRes);
if (res.error) {
throw new Error(
'failed to get accounts owned by program ' +
programId.toBase58() +
': ' +
res.error.message,
);
}
const {result} = res;
assert(typeof result !== 'undefined');
return result.map(result => {
const resultData = result.account.data;
let data = resultData;
if (!data.program) {
data = bs58.decode(data);
}
return {
pubkey: new PublicKey(result.pubkey),
account: {
executable: result.account.executable,
owner: new PublicKey(result.account.owner),
lamports: result.account.lamports,
data,
},
};
});
}
/**
* Confirm the transaction identified by the specified signature
*/
@ -2625,10 +2850,21 @@ export class Connection {
}
}
_argsWithCommitment(args: Array<any>, override: ?Commitment): Array<any> {
_argsWithCommitment(
args: Array<any>,
override: ?Commitment,
encoding?: 'jsonParsed',
): Array<any> {
const commitment = override || this._commitment;
if (commitment) {
args.push({commitment});
if (commitment || encoding) {
let options: any = {};
if (encoding) {
options.encoding = encoding;
}
if (commitment) {
options.commitment = commitment;
}
args.push(options);
}
return args;
}

View File

@ -77,6 +77,12 @@ test('get account info - not found', async () => {
]);
expect(await connection.getAccountInfo(account.publicKey)).toBeNull();
if (!mockRpcEnabled) {
expect(
(await connection.getParsedAccountInfo(account.publicKey)).value,
).toBeNull();
}
});
test('get program accounts', async () => {
@ -282,20 +288,39 @@ test('get program accounts', async () => {
expect(programAccounts.length).toBe(2);
programAccounts.forEach(function (element) {
expect([
account0.publicKey.toBase58(),
account1.publicKey.toBase58(),
]).toEqual(expect.arrayContaining([element.pubkey]));
if (element.pubkey == account0.publicKey) {
if (element.pubkey.equals(account0.publicKey)) {
expect(element.account.lamports).toBe(
LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
);
} else {
} else if (element.pubkey.equals(account1.publicKey)) {
expect(element.account.lamports).toBe(
0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
);
} else {
expect(element.pubkey.equals(account1.publicKey)).toBe(true);
}
});
if (!mockRpcEnabled) {
const programAccounts = await connection.getParsedProgramAccounts(
programId.publicKey,
);
expect(programAccounts.length).toBe(2);
programAccounts.forEach(function (element) {
if (element.pubkey.equals(account0.publicKey)) {
expect(element.account.lamports).toBe(
LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
);
} else if (element.pubkey.equals(account1.publicKey)) {
expect(element.account.lamports).toBe(
0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature,
);
} else {
expect(element.pubkey.equals(account1.publicKey)).toBe(true);
}
});
}
});
test('validatorExit', async () => {
@ -1410,6 +1435,55 @@ describe('token methods', () => {
).rejects.toThrow();
});
test('get parsed token account info', async () => {
const accountInfo = (
await connection.getParsedAccountInfo(testTokenAccount)
).value;
if (accountInfo) {
const data = accountInfo.data;
if (data instanceof Buffer) {
expect(data instanceof Buffer).toBe(false);
} else {
expect(data.program).toEqual('spl-token');
expect(data.parsed).toBeTruthy();
}
}
});
test('get parsed token program accounts', async () => {
const tokenAccounts = await connection.getParsedProgramAccounts(
TOKEN_PROGRAM_ID,
);
tokenAccounts.forEach(({account}) => {
expect(account.owner.equals(TOKEN_PROGRAM_ID)).toBe(true);
const data = account.data;
if (data instanceof Buffer) {
expect(data instanceof Buffer).toBe(false);
} else {
expect(data.parsed).toBeTruthy();
expect(data.program).toEqual('spl-token');
}
});
});
test('get parsed token accounts by owner', async () => {
const tokenAccounts = (
await connection.getParsedTokenAccountsByOwner(testOwner.publicKey, {
mint: testToken.publicKey,
})
).value;
tokenAccounts.forEach(({account}) => {
expect(account.owner.equals(TOKEN_PROGRAM_ID)).toBe(true);
const data = account.data;
if (data instanceof Buffer) {
expect(data instanceof Buffer).toBe(false);
} else {
expect(data.parsed).toBeTruthy();
expect(data.program).toEqual('spl-token');
}
});
});
test('get token accounts by owner', async () => {
const accountsWithMintFilter = (
await connection.getTokenAccountsByOwner(testOwner.publicKey, {
@ -1611,6 +1685,45 @@ test('request airdrop', async () => {
expect(accountInfo.lamports).toBe(minimumAmount + 42);
expect(accountInfo.data).toHaveLength(0);
expect(accountInfo.owner).toEqual(SystemProgram.programId);
mockRpc.push([
url,
{
method: 'getAccountInfo',
params: [
account.publicKey.toBase58(),
{commitment: 'recent', encoding: 'jsonParsed'},
],
},
{
error: null,
result: {
context: {
slot: 11,
},
value: {
owner: '11111111111111111111111111111111',
lamports: minimumAmount + 42,
data: '',
executable: false,
},
},
},
]);
const parsedAccountInfo = (
await connection.getParsedAccountInfo(account.publicKey)
).value;
if (parsedAccountInfo === null) {
expect(parsedAccountInfo).not.toBeNull();
return;
} else if (parsedAccountInfo.data.parsed) {
expect(parsedAccountInfo.data.parsed).not.toBeTruthy();
return;
}
expect(parsedAccountInfo.lamports).toBe(minimumAmount + 42);
expect(parsedAccountInfo.data).toHaveLength(0);
expect(parsedAccountInfo.owner).toEqual(SystemProgram.programId);
});
test('transaction failure', async () => {