Skip to content

Commit

Permalink
refactor(experimental): GraphQL: Refactor program accounts filters (#…
Browse files Browse the repository at this point in the history
…3098)

* refactor(experimental): rpc-api: add tests for `getProgramAccounts` filters

* refactor(experimental): graphql: refactor `programAccounts` filters

* refactor(experimental): graphql: add tests for `programAccounts` filters

* add changeset
  • Loading branch information
buffalojoec authored Aug 14, 2024
1 parent 2f91026 commit 2f541b6
Show file tree
Hide file tree
Showing 10 changed files with 410 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .changeset/sixty-months-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/rpc-graphql': minor
---

Update program accounts filters for `programAccounts` query
170 changes: 170 additions & 0 deletions packages/rpc-api/src/__tests__/get-program-accounts-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2290,4 +2290,174 @@ describe('getProgramAccounts', () => {
expect(accountInfo[0].account.data).toStrictEqual(['dGVzdCA=', 'base64']);
});
});

describe('when called with a data size filter', () => {
it('returns the matching accounts', async () => {
expect.assertions(3);
const program =
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' as Address<'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'>;

const programAccounts = await rpc
.getProgramAccounts(program, {
encoding: 'jsonParsed',
filters: [
{
dataSize: 165n, // Token account size
},
],
})
.send();

programAccounts.forEach(item => {
expect(item).toMatchObject({
account: {
data: {
parsed: {
info: {
isNative: expect.any(Boolean),
mint: expect.any(String),
owner: expect.any(String),
state: expect.any(String),
tokenAmount: {
amount: expect.any(String),
decimals: expect.any(Number),
uiAmount: expect.any(Number),
uiAmountString: expect.any(String),
},
},
type: 'account',
},
program: 'spl-token',
space: 165n, // Token account space
},
executable: false,
lamports: expect.any(BigInt),
owner: expect.any(String),
rentEpoch: expect.any(BigInt),
space: 165n, // Token account space
},
pubkey: expect.any(String),
});
});
});
});

describe('when called with a memcmpy filter', () => {
it('returns the matching accounts', async () => {
expect.assertions(1);
// See scripts/fixtures/spl-token-mint-account.json
const mint =
'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr' as Address<'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr'>;
const program =
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' as Address<'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'>;

const programAccounts = await rpc
.getProgramAccounts(program, {
encoding: 'jsonParsed',
filters: [
{
memcmp: {
bytes: mint,
encoding: 'base58',
offset: 0n,
},
},
],
})
.send();

programAccounts.forEach(item => {
expect(item).toMatchObject({
account: {
data: {
parsed: {
info: {
isNative: expect.any(Boolean),
mint, // Matches mint address provided in filter.
owner: expect.any(String),
state: expect.any(String),
tokenAmount: {
amount: expect.any(String),
decimals: expect.any(Number),
uiAmount: expect.any(Number),
uiAmountString: expect.any(String),
},
},
type: 'account',
},
program: 'spl-token',
space: 165n, // Token account space
},
executable: false,
lamports: expect.any(BigInt),
owner: expect.any(String),
rentEpoch: expect.any(BigInt),
space: 165n, // Token account space
},
pubkey: expect.any(String),
});
});
});
});

describe('when called with both a data size and a memcmpy filter', () => {
it('returns the matching accounts', async () => {
expect.assertions(1);
// See scripts/fixtures/spl-token-mint-account.json
const mint =
'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr' as Address<'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr'>;
const program =
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' as Address<'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'>;

const programAccounts = await rpc
.getProgramAccounts(program, {
encoding: 'jsonParsed',
filters: [
{
dataSize: 165n, // Token account size
},
{
memcmp: {
bytes: mint,
encoding: 'base58',
offset: 0n,
},
},
],
})
.send();

programAccounts.forEach(item => {
expect(item).toMatchObject({
account: {
data: {
parsed: {
info: {
isNative: expect.any(Boolean),
mint, // Matches mint address provided in filter.
owner: expect.any(String),
state: expect.any(String),
tokenAmount: {
amount: expect.any(String),
decimals: expect.any(Number),
uiAmount: expect.any(Number),
uiAmountString: expect.any(String),
},
},
type: 'account',
},
program: 'spl-token',
space: 165n, // Token account space
},
executable: false,
lamports: expect.any(BigInt),
owner: expect.any(String),
rentEpoch: expect.any(BigInt),
space: 165n, // Token account space
},
pubkey: expect.any(String),
});
});
});
});
});
161 changes: 161 additions & 0 deletions packages/rpc-graphql/src/__tests__/program-accounts-test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Address } from '@solana/addresses';
import {
GetAccountInfoApi,
GetBlockApi,
Expand Down Expand Up @@ -617,4 +618,164 @@ describe('programAccounts', () => {
});
});
});
describe('when called with a data size filter', () => {
const programAddress =
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' as Address<'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'>;

describe('when using memcmp filter', () => {
it('returns the matching accounts', async () => {
expect.assertions(1);
const variableValues = {
dataSizeFilters: [
{
dataSize: 165n, // Token account size
},
],
programAddress,
};
const source = /* GraphQL */ `
query testQuery($programAddress: Address!, $dataSizeFilters: [ProgramAccountsDataSizeFilter!]!) {
programAccounts(
programAddress: $programAddress
commitment: null
dataSizeFilters: $dataSizeFilters
) {
... on TokenAccount {
mint {
address
}
}
}
}
`;
const result = await rpcGraphQL.query(source, variableValues);
console.log(result);
expect(result).toMatchObject({
data: {
programAccounts: expect.arrayContaining([
{
mint: {
address: expect.any(String),
},
},
]),
},
});
});
});
});
describe('when called with a memcmp filter', () => {
// See scripts/fixtures/spl-token-mint-account.json
const mintAddress =
'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr' as Address<'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr'>;
const programAddress =
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' as Address<'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'>;

describe('when using memcmp filter', () => {
it('returns the matching accounts', async () => {
expect.assertions(1);
const variableValues = {
memcmpFilters: [
{
bytes: mintAddress, // Mint address in data.
encoding: 'BASE_58', // Base58-encoded address.
offset: 0, // Offset 0 for mint address.
},
],
programAddress,
};
const source = /* GraphQL */ `
query testQuery($programAddress: Address!, $memcmpFilters: [ProgramAccountsMemcmpFilter!]!) {
programAccounts(
programAddress: $programAddress
commitment: null
dataSizeFilters: null
memcmpFilters: $memcmpFilters
) {
... on TokenAccount {
mint {
address
}
}
}
}
`;
const result = await rpcGraphQL.query(source, variableValues);
console.log(result);
expect(result).toMatchObject({
data: {
programAccounts: expect.arrayContaining([
{
mint: {
address: mintAddress,
},
},
]),
},
});
});
});
});

describe('when called with both a data size and a memcmpy filter', () => {
// See scripts/fixtures/spl-token-mint-account.json
const mintAddress =
'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr' as Address<'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr'>;
const programAddress =
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' as Address<'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'>;

describe('when using memcmp filter', () => {
it('returns the matching accounts', async () => {
expect.assertions(1);
const variableValues = {
dataSizeFilters: [
{
dataSize: 165n, // Token account size
},
],
memcmpFilters: [
{
bytes: mintAddress, // Mint address in data.
encoding: 'BASE_58', // Base58-encoded address.
offset: 0, // Offset 0 for mint address.
},
],
programAddress,
};
const source = /* GraphQL */ `
query testQuery(
$programAddress: Address!
$dataSizeFilters: [ProgramAccountsDataSizeFilter!]!
$memcmpFilters: [ProgramAccountsMemcmpFilter!]!
) {
programAccounts(
programAddress: $programAddress
commitment: null
dataSizeFilters: $dataSizeFilters
memcmpFilters: $memcmpFilters
) {
... on TokenAccount {
mint {
address
}
}
}
}
`;
const result = await rpcGraphQL.query(source, variableValues);
console.log(result);
expect(result).toMatchObject({
data: {
programAccounts: expect.arrayContaining([
{
mint: {
address: mintAddress,
},
},
]),
},
});
});
});
});
});
13 changes: 12 additions & 1 deletion packages/rpc-graphql/src/loaders/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,18 @@ export type ProgramAccountsLoaderArgsBase = {
commitment?: Commitment;
dataSlice?: { length: number; offset: number };
encoding?: 'base58' | 'base64' | 'base64+zstd' | 'jsonParsed';
filters?: readonly { memcmp: { bytes: string; offset: number } }[];
filters?: (
| {
dataSize: bigint;
}
| {
memcmp: {
bytes: string;
encoding: 'base58' | 'base64';
offset: bigint;
};
}
)[];
minContextSlot?: Slot;
};
export type ProgramAccountsLoaderArgs = ProgramAccountsLoaderArgsBase & { programAddress: Address };
Expand Down
9 changes: 1 addition & 8 deletions packages/rpc-graphql/src/loaders/program-accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,7 @@ async function loadProgramAccounts(
rpc: Rpc<GetProgramAccountsApi>,
{ programAddress, ...config }: ProgramAccountsLoaderArgs,
): Promise<ProgramAccountsLoaderValue> {
// @ts-expect-error FIX ME: https://github.com/microsoft/TypeScript/issues/43187
return await rpc
.getProgramAccounts(
programAddress,
// @ts-expect-error FIX ME: https://github.com/microsoft/TypeScript/issues/43187
config,
)
.send();
return await rpc.getProgramAccounts(programAddress, config).send();
}

function createProgramAccountsBatchLoadFn(rpc: Rpc<GetProgramAccountsApi>, config: Config) {
Expand Down
Loading

0 comments on commit 2f541b6

Please sign in to comment.