From 8dfdf6b5f2c983156ac12cbdd21e1c955c2be41d Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 16 Apr 2021 10:18:19 -0700 Subject: [PATCH] feat: add filters to getProgramAccounts and getParsedProgramAccounts (#16448) * feat: add filters to getProgramAccounts and getParsedProgramAccounts * fix: documentation edits * fix: make connection interface match existing interface --- src/connection.ts | 104 +++++++++++++++++- test/connection.test.ts | 234 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 334 insertions(+), 4 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 1048cfa0ed2..1faddac6ca4 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1368,6 +1368,63 @@ export type StakeActivationData = { inactive: number; }; +/** + * Data slice argument for getProgramAccounts + */ +export type DataSlice = { + /** offset of data slice */ + offset: number; + /** length of data slice */ + length: number; +}; + +/** + * Memory comparison filter for getProgramAccounts + */ +export type MemcmpFilter = { + memcmp: { + /** offset into program account data to start comparison */ + offset: number; + /** data to match, as base-58 encoded string and limited to less than 129 bytes */ + bytes: string; + }; +}; + +/** + * Data size comparison filter for getProgramAccounts + */ +export type DataSizeFilter = { + /** Size of data for program account data length comparison */ + dataSize: number; +}; + +/** + * A filter object for getProgramAccounts + */ +export type GetProgramAccountsFilter = MemcmpFilter | DataSizeFilter; + +/** + * Configuration object for getProgramAccounts requests + */ +export type GetProgramAccountsConfig = { + /** Optional commitment level */ + commitment?: Commitment; + /** Optional encoding for account data (default base64) */ + encoding?: 'base64' | 'jsonParsed'; + /** Optional data slice to limit the returned account data */ + dataSlice?: DataSlice; + /** Optional array of filters to apply to accounts */ + filters?: GetProgramAccountsFilter[]; +}; + +/** + * Configuration object for getParsedProgramAccounts + */ +export type GetParsedProgramAccountsConfig = Exclude< + GetProgramAccountsConfig, + 'encoding' | 'dataSlice' +>; + /** * Information describing an account */ @@ -2080,9 +2137,34 @@ export class Connection { */ async getProgramAccounts( programId: PublicKey, - commitment?: Commitment, + configOrCommitment?: GetProgramAccountsConfig | Commitment, ): Promise}>> { - const args = this._buildArgs([programId.toBase58()], commitment, 'base64'); + const extra: Pick = {}; + + let commitment; + let encoding; + if (configOrCommitment) { + if (typeof configOrCommitment === 'string') { + commitment = configOrCommitment; + } else { + commitment = configOrCommitment.commitment; + encoding = configOrCommitment.encoding; + + if (configOrCommitment.dataSlice) { + extra.dataSlice = configOrCommitment.dataSlice; + } + if (configOrCommitment.filters) { + extra.filters = configOrCommitment.filters; + } + } + } + + const args = this._buildArgs( + [programId.toBase58()], + commitment, + encoding || 'base64', + extra, + ); const unsafeRes = await this._rpcRequest('getProgramAccounts', args); const res = create(unsafeRes, jsonRpcResult(array(KeyedAccountInfoResult))); if ('error' in res) { @@ -2103,17 +2185,33 @@ export class Connection { */ async getParsedProgramAccounts( programId: PublicKey, - commitment?: Commitment, + configOrCommitment?: GetParsedProgramAccountsConfig | Commitment, ): Promise< Array<{ pubkey: PublicKey; account: AccountInfo; }> > { + const extra: Pick = {}; + + let commitment; + if (configOrCommitment) { + if (typeof configOrCommitment === 'string') { + commitment = configOrCommitment; + } else { + commitment = configOrCommitment.commitment; + + if (configOrCommitment.filters) { + extra.filters = configOrCommitment.filters; + } + } + } + const args = this._buildArgs( [programId.toBase58()], commitment, 'jsonParsed', + extra, ); const unsafeRes = await this._rpcRequest('getProgramAccounts', args); const res = create( diff --git a/test/connection.test.ts b/test/connection.test.ts index 2a335e640ed..1da065fcef1 100644 --- a/test/connection.test.ts +++ b/test/connection.test.ts @@ -163,6 +163,59 @@ describe('Connection', () => { const feeCalculator = (await helpers.recentBlockhash({connection})) .feeCalculator; + { + await mockRpcResponse({ + method: 'getProgramAccounts', + params: [ + programId.publicKey.toBase58(), + {commitment: 'confirmed', encoding: 'base64'}, + ], + value: [ + { + account: { + data: ['', 'base64'], + executable: false, + lamports: LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature, + owner: programId.publicKey.toBase58(), + rentEpoch: 20, + }, + pubkey: account0.publicKey.toBase58(), + }, + { + account: { + data: ['', 'base64'], + executable: false, + lamports: + 0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature, + owner: programId.publicKey.toBase58(), + rentEpoch: 20, + }, + pubkey: account1.publicKey.toBase58(), + }, + ], + }); + + const programAccounts = await connection.getProgramAccounts( + programId.publicKey, + { + commitment: 'confirmed', + }, + ); + expect(programAccounts).to.have.length(2); + programAccounts.forEach(function (keyedAccount) { + if (keyedAccount.pubkey.equals(account0.publicKey)) { + expect(keyedAccount.account.lamports).to.eq( + LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature, + ); + } else { + expect(keyedAccount.pubkey).to.eql(account1.publicKey); + expect(keyedAccount.account.lamports).to.eq( + 0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature, + ); + } + }); + } + { await mockRpcResponse({ method: 'getProgramAccounts', @@ -214,6 +267,95 @@ describe('Connection', () => { }); } + { + await mockRpcResponse({ + method: 'getProgramAccounts', + params: [ + programId.publicKey.toBase58(), + { + commitment: 'confirmed', + encoding: 'base64', + filters: [ + { + dataSize: 0, + }, + ], + }, + ], + value: [ + { + account: { + data: ['', 'base64'], + executable: false, + lamports: LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature, + owner: programId.publicKey.toBase58(), + rentEpoch: 20, + }, + pubkey: account0.publicKey.toBase58(), + }, + { + account: { + data: ['', 'base64'], + executable: false, + lamports: + 0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature, + owner: programId.publicKey.toBase58(), + rentEpoch: 20, + }, + pubkey: account1.publicKey.toBase58(), + }, + ], + }); + + const programAccountsDoMatchFilter = await connection.getProgramAccounts( + programId.publicKey, + { + commitment: 'confirmed', + encoding: 'base64', + filters: [{dataSize: 0}], + }, + ); + expect(programAccountsDoMatchFilter).to.have.length(2); + } + + { + await mockRpcResponse({ + method: 'getProgramAccounts', + params: [ + programId.publicKey.toBase58(), + { + commitment: 'confirmed', + encoding: 'base64', + filters: [ + { + memcmp: { + offset: 0, + bytes: 'XzdZ3w', + }, + }, + ], + }, + ], + value: [], + }); + + const programAccountsDontMatchFilter = await connection.getProgramAccounts( + programId.publicKey, + { + commitment: 'confirmed', + filters: [ + { + memcmp: { + offset: 0, + bytes: 'XzdZ3w', + }, + }, + ], + }, + ); + expect(programAccountsDontMatchFilter).to.have.length(0); + } + { await mockRpcResponse({ method: 'getProgramAccounts', @@ -248,7 +390,9 @@ describe('Connection', () => { const programAccounts = await connection.getParsedProgramAccounts( programId.publicKey, - 'confirmed', + { + commitment: 'confirmed', + }, ); expect(programAccounts).to.have.length(2); @@ -265,6 +409,94 @@ describe('Connection', () => { } }); } + + { + await mockRpcResponse({ + method: 'getProgramAccounts', + params: [ + programId.publicKey.toBase58(), + { + commitment: 'confirmed', + encoding: 'jsonParsed', + filters: [ + { + dataSize: 2, + }, + ], + }, + ], + value: [], + }); + + const programAccountsDontMatchFilter = await connection.getParsedProgramAccounts( + programId.publicKey, + { + commitment: 'confirmed', + filters: [{dataSize: 2}], + }, + ); + expect(programAccountsDontMatchFilter).to.have.length(0); + } + + { + await mockRpcResponse({ + method: 'getProgramAccounts', + params: [ + programId.publicKey.toBase58(), + { + commitment: 'confirmed', + encoding: 'jsonParsed', + filters: [ + { + memcmp: { + offset: 0, + bytes: '', + }, + }, + ], + }, + ], + value: [ + { + account: { + data: ['', 'base64'], + executable: false, + lamports: LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature, + owner: programId.publicKey.toBase58(), + rentEpoch: 20, + }, + pubkey: account0.publicKey.toBase58(), + }, + { + account: { + data: ['', 'base64'], + executable: false, + lamports: + 0.5 * LAMPORTS_PER_SOL - feeCalculator.lamportsPerSignature, + owner: programId.publicKey.toBase58(), + rentEpoch: 20, + }, + pubkey: account1.publicKey.toBase58(), + }, + ], + }); + + const programAccountsDoMatchFilter = await connection.getParsedProgramAccounts( + programId.publicKey, + { + commitment: 'confirmed', + filters: [ + { + memcmp: { + offset: 0, + bytes: '', + }, + }, + ], + }, + ); + expect(programAccountsDoMatchFilter).to.have.length(2); + } }); it('get balance', async () => {