Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
refactor(experimental): graphql: expand on batch loader for accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
buffalojoec committed Feb 22, 2024
1 parent 10aedf8 commit 6dc0483
Show file tree
Hide file tree
Showing 12 changed files with 2,127 additions and 62 deletions.
1 change: 1 addition & 0 deletions packages/rpc-graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
],
"dependencies": {
"@graphql-tools/schema": "^10.0.0",
"@solana/codecs-strings": "workspace:*",
"dataloader": "^2.2.2",
"graphql": "^16.8.0",
"json-stable-stringify": "^1.1.0"
Expand Down
168 changes: 166 additions & 2 deletions packages/rpc-graphql/src/__tests__/account-test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { GetAccountInfoApi, GetBlockApi, GetProgramAccountsApi, GetTransactionApi, Rpc } from '@solana/rpc';
import {
GetAccountInfoApi,
GetBlockApi,
GetMultipleAccountsApi,
GetProgramAccountsApi,
GetTransactionApi,
Rpc,
} from '@solana/rpc';
import fetchMock from 'jest-fetch-mock-fork';

import { createRpcGraphQL, RpcGraphQL } from '../index';
import { createLocalhostSolanaRpc } from './__setup__';

describe('account', () => {
let rpc: Rpc<GetAccountInfoApi & GetBlockApi & GetProgramAccountsApi & GetTransactionApi>;
let rpc: Rpc<GetAccountInfoApi & GetBlockApi & GetMultipleAccountsApi & GetProgramAccountsApi & GetTransactionApi>;
let rpcGraphQL: RpcGraphQL;
beforeEach(() => {
fetchMock.resetMocks();
Expand Down Expand Up @@ -242,6 +249,31 @@ describe('account', () => {
},
});
});
it('can get account data as base58 with data slice', async () => {
expect.assertions(1);
// See scripts/fixtures/gpa1.json
const variableValues = {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
encoding: 'BASE_58',
};
const source = /* GraphQL */ `
query testQuery($address: Address!, $encoding: AccountEncoding!) {
account(address: $address) {
address
data(encoding: $encoding, dataSlice: { offset: 0, length: 5 })
}
}
`;
const result = await rpcGraphQL.query(source, variableValues);
expect(result).toMatchObject({
data: {
account: {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
data: 'E8f4pET', // As tested on local RPC
},
},
});
});
it('can get account data as base64', async () => {
expect.assertions(1);
const source = /* GraphQL */ `
Expand All @@ -262,6 +294,58 @@ describe('account', () => {
},
});
});
it('can get account data as base64 with data slice', async () => {
expect.assertions(1);
// See scripts/fixtures/gpa1.json
const variableValues = {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
encoding: 'BASE_64',
};
const source = /* GraphQL */ `
query testQuery($address: Address!, $encoding: AccountEncoding!) {
account(address: $address) {
address
data(encoding: $encoding, dataSlice: { offset: 0, length: 5 })
}
}
`;
const result = await rpcGraphQL.query(source, variableValues);
expect(result).toMatchObject({
data: {
account: {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
data: 'dGVzdCA=',
},
},
});
});
it('can get account data as base64 with two data slices', async () => {
expect.assertions(1);
// See scripts/fixtures/gpa1.json
const variableValues = {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
encoding: 'BASE_64',
};
const source = /* GraphQL */ `
query testQuery($address: Address!, $encoding: AccountEncoding!) {
account(address: $address) {
address
dataA: data(encoding: $encoding, dataSlice: { offset: 2, length: 5 })
dataB: data(encoding: $encoding, dataSlice: { offset: 4, length: 5 })
}
}
`;
const result = await rpcGraphQL.query(source, variableValues);
expect(result).toMatchObject({
data: {
account: {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
dataA: 'c3QgZGE=', // As tested on local RPC
dataB: 'IGRhdGE=', // As tested on local RPC
},
},
});
});
it('can get account data as base64+zstd', async () => {
expect.assertions(1);
const source = /* GraphQL */ `
Expand All @@ -282,6 +366,58 @@ describe('account', () => {
},
});
});
it('can get account data as base64+zstd with data slice', async () => {
expect.assertions(1);
// See scripts/fixtures/gpa1.json
const variableValues = {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
encoding: 'BASE_64_ZSTD',
};
const source = /* GraphQL */ `
query testQuery($address: Address!, $encoding: AccountEncoding!) {
account(address: $address) {
address
data(encoding: $encoding, dataSlice: { offset: 0, length: 15 })
}
}
`;
const result = await rpcGraphQL.query(source, variableValues);
expect(result).toMatchObject({
data: {
account: {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
data: 'KLUv/QBYSQAAdGVzdCBkYXRh', // As tested on local RPC
},
},
});
});
it('can get account data as base64+zstd with multiple data slices', async () => {
expect.assertions(1);
// See scripts/fixtures/gpa1.json
const variableValues = {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
encoding: 'BASE_64_ZSTD',
};
const source = /* GraphQL */ `
query testQuery($address: Address!, $encoding: AccountEncoding!) {
account(address: $address) {
address
dataA: data(encoding: $encoding, dataSlice: { offset: 0, length: 5 })
dataB: data(encoding: $encoding, dataSlice: { offset: 0, length: 15 })
}
}
`;
const result = await rpcGraphQL.query(source, variableValues);
expect(result).toMatchObject({
data: {
account: {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
dataA: 'KLUv/QBYKQAAdGVzdCA=', // As tested on local RPC
dataB: 'KLUv/QBYSQAAdGVzdCBkYXRh', // As tested on local RPC
},
},
});
});
it('can get account data with multiple encodings', async () => {
expect.assertions(1);
const source = /* GraphQL */ `
Expand All @@ -306,6 +442,34 @@ describe('account', () => {
},
});
});
it('can get account data with multiple encodings and data slices', async () => {
expect.assertions(1);
// See scripts/fixtures/gpa1.json
const variableValues = {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
};
const source = /* GraphQL */ `
query testQuery($address: Address!) {
account(address: $address) {
address
dataBase58: data(encoding: BASE_58, dataSlice: { offset: 0, length: 5 })
dataBase64: data(encoding: BASE_64, dataSlice: { offset: 0, length: 5 })
dataBase64Zstd: data(encoding: BASE_64_ZSTD, dataSlice: { offset: 0, length: 5 })
}
}
`;
const result = await rpcGraphQL.query(source, variableValues);
expect(result).toMatchObject({
data: {
account: {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
dataBase58: 'E8f4pET', // As tested on local RPC
dataBase64: 'dGVzdCA=', // As tested on local RPC
dataBase64Zstd: 'KLUv/QBYKQAAdGVzdCA=', // As tested on local RPC
},
},
});
});
it('can get account data as jsonParsed', async () => {
expect.assertions(1);
const source = /* GraphQL */ `
Expand Down
11 changes: 9 additions & 2 deletions packages/rpc-graphql/src/__tests__/block-tests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { GetAccountInfoApi, GetBlockApi, GetProgramAccountsApi, GetTransactionApi, Rpc } from '@solana/rpc';
import {
GetAccountInfoApi,
GetBlockApi,
GetMultipleAccountsApi,
GetProgramAccountsApi,
GetTransactionApi,
Rpc,
} from '@solana/rpc';
import type { Slot } from '@solana/rpc-types';
import fetchMock from 'jest-fetch-mock-fork';

Expand All @@ -15,7 +22,7 @@ import {
} from './__setup__';

describe('block', () => {
let rpc: Rpc<GetAccountInfoApi & GetBlockApi & GetProgramAccountsApi & GetTransactionApi>;
let rpc: Rpc<GetAccountInfoApi & GetBlockApi & GetMultipleAccountsApi & GetProgramAccountsApi & GetTransactionApi>;
let rpcGraphQL: RpcGraphQL;

// Random slot for testing.
Expand Down
15 changes: 11 additions & 4 deletions packages/rpc-graphql/src/__tests__/program-accounts-test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { GetAccountInfoApi, GetBlockApi, GetProgramAccountsApi, GetTransactionApi, Rpc } from '@solana/rpc';
import {
GetAccountInfoApi,
GetBlockApi,
GetMultipleAccountsApi,
GetProgramAccountsApi,
GetTransactionApi,
Rpc,
} from '@solana/rpc';
import fetchMock from 'jest-fetch-mock-fork';

import { createRpcGraphQL, RpcGraphQL } from '../index';
import { createLocalhostSolanaRpc } from './__setup__';

describe('programAccounts', () => {
let rpc: Rpc<GetAccountInfoApi & GetBlockApi & GetProgramAccountsApi & GetTransactionApi>;
let rpc: Rpc<GetAccountInfoApi & GetBlockApi & GetMultipleAccountsApi & GetProgramAccountsApi & GetTransactionApi>;
let rpcGraphQL: RpcGraphQL;
beforeEach(() => {
fetchMock.resetMocks();
Expand Down Expand Up @@ -589,7 +596,7 @@ describe('programAccounts', () => {
data: {
programAccounts: expect.arrayContaining([
{
data: 'E8f4pET',
data: 'E8f4pET', // As tested on local RPC
},
]),
},
Expand Down Expand Up @@ -632,7 +639,7 @@ describe('programAccounts', () => {
data: {
programAccounts: expect.arrayContaining([
{
data: 'dGVzdCA=',
data: 'dGVzdCA=', // As tested on local RPC
},
]),
},
Expand Down
11 changes: 9 additions & 2 deletions packages/rpc-graphql/src/__tests__/transaction-tests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Signature } from '@solana/keys';
import { GetAccountInfoApi, GetBlockApi, GetProgramAccountsApi, GetTransactionApi, Rpc } from '@solana/rpc';
import {
GetAccountInfoApi,
GetBlockApi,
GetMultipleAccountsApi,
GetProgramAccountsApi,
GetTransactionApi,
Rpc,
} from '@solana/rpc';
import fetchMock from 'jest-fetch-mock-fork';

import { createRpcGraphQL, RpcGraphQL } from '../index';
Expand All @@ -17,7 +24,7 @@ import {
} from './__setup__';

describe('transaction', () => {
let rpc: Rpc<GetAccountInfoApi & GetBlockApi & GetProgramAccountsApi & GetTransactionApi>;
let rpc: Rpc<GetAccountInfoApi & GetBlockApi & GetMultipleAccountsApi & GetProgramAccountsApi & GetTransactionApi>;
let rpcGraphQL: RpcGraphQL;

// Random signature for testing.
Expand Down
30 changes: 26 additions & 4 deletions packages/rpc-graphql/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import type { createRpcGraphQL } from './index';
import type {
GetAccountInfoApi,
GetBlockApi,
GetMultipleAccountsApi,
GetProgramAccountsApi,
GetTransactionApi,
Rpc,
} from '@solana/rpc';

import {
createAccountLoader,
createBlockLoader,
Expand All @@ -7,16 +15,30 @@ import {
RpcGraphQLLoaders,
} from './loaders';

export type Rpc = Parameters<typeof createRpcGraphQL>[0];
type Config = {
/**
* Maximum number of acceptable bytes to waste before splitting two
* `dataSlice` requests into two requests.
*/
maxDataSliceByteRange: number;
/**
* Maximum number of accounts to fetch in a single batch.
* See https://docs.solana.com/api/http#getmultipleaccounts.
*/
maxMultipleAccountsBatchSize: number;
};

export interface RpcGraphQLContext {
loaders: RpcGraphQLLoaders;
}

export function createSolanaGraphQLContext(rpc: Rpc): RpcGraphQLContext {
export function createSolanaGraphQLContext(
rpc: Rpc<GetAccountInfoApi & GetBlockApi & GetMultipleAccountsApi & GetProgramAccountsApi & GetTransactionApi>,
config: Config,
): RpcGraphQLContext {
return {
loaders: {
account: createAccountLoader(rpc),
account: createAccountLoader(rpc, config),
block: createBlockLoader(rpc),
programAccounts: createProgramAccountsLoader(rpc),
transaction: createTransactionLoader(rpc),
Expand Down
14 changes: 9 additions & 5 deletions packages/rpc-graphql/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import { makeExecutableSchema } from '@graphql-tools/schema';
import type { GetAccountInfoApi, GetBlockApi, GetProgramAccountsApi, GetTransactionApi, Rpc } from '@solana/rpc';
import { graphql } from 'graphql';

import { createSolanaGraphQLContext } from './context';
import { createSolanaGraphQLResolvers } from './resolvers';
import { createSolanaGraphQLTypeDefs } from './schema';

type RpcMethods = GetAccountInfoApi & GetBlockApi & GetProgramAccountsApi & GetTransactionApi;

export interface RpcGraphQL {
query(
source: Parameters<typeof graphql>[0]['source'],
variableValues?: Parameters<typeof graphql>[0]['variableValues'],
): ReturnType<typeof graphql>;
}

export function createRpcGraphQL(rpc: Rpc<RpcMethods>): RpcGraphQL {
export function createRpcGraphQL(
rpc: Parameters<typeof createSolanaGraphQLContext>[0],
config?: Partial<Parameters<typeof createSolanaGraphQLContext>[1]>,
): RpcGraphQL {
const rpcGraphQLConfig = {
maxDataSliceByteRange: config?.maxDataSliceByteRange ?? 200,
maxMultipleAccountsBatchSize: config?.maxMultipleAccountsBatchSize ?? 100,
};
const schema = makeExecutableSchema({
resolvers: createSolanaGraphQLResolvers(),
typeDefs: createSolanaGraphQLTypeDefs(),
});
return {
async query(source, variableValues?) {
const contextValue = createSolanaGraphQLContext(rpc);
const contextValue = createSolanaGraphQLContext(rpc, rpcGraphQLConfig);
return graphql({
contextValue,
schema,
Expand Down
Loading

0 comments on commit 6dc0483

Please sign in to comment.