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

refactor(experimental): add bigint support to fast-stable-stringify #2014

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 111 additions & 50 deletions packages/rpc-graphql/src/__tests__/block-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,72 @@ describe('block', () => {
rpcGraphQL = createRpcGraphQL(rpc);
});

// The `block` query takes a `BigInt` as a parameter. We need to test this
// for various input types that might occur outside of a JavaScript
// context, such as string or number.
describe('bigint parameter', () => {
const source = /* GraphQL */ `
query testQuery($block: BigInt!) {
block(slot: $block) {
blockhash
}
}
`;
1;
it('can accept a bigint parameter', async () => {
expect.assertions(2);
fetchMock.mockOnce(JSON.stringify(mockRpcResponse(mockBlockNone)));
const variables = { block: 511226n };
const result = await rpcGraphQL.query(source, variables);
expect(result).not.toHaveProperty('errors');
expect(result).toMatchObject({
data: {
block: {
blockhash: expect.any(String),
},
},
});
});
it('can accept a number parameter', async () => {
expect.assertions(2);
fetchMock.mockOnce(JSON.stringify(mockRpcResponse(mockBlockNone)));
const variables = { block: 511226 };
const result = await rpcGraphQL.query(source, variables);
expect(result).not.toHaveProperty('errors');
expect(result).toMatchObject({
data: {
block: {
blockhash: expect.any(String),
},
},
});
});
it('can accept a string parameter', async () => {
expect.assertions(2);
fetchMock.mockOnce(JSON.stringify(mockRpcResponse(mockBlockNone)));
const variables = { block: '511226' };
const result = await rpcGraphQL.query(source, variables);
expect(result).not.toHaveProperty('errors');
expect(result).toMatchObject({
data: {
block: {
blockhash: expect.any(String),
},
},
});
});
});
describe('basic queries', () => {
it("can query a block's block time", async () => {
expect.assertions(1);
fetchMock.mockOnce(JSON.stringify(mockRpcResponse(mockBlockFull)));
const source = /* GraphQL */ `
query testQuery {
block(slot: ${defaultSlot}) {
blockTime
query testQuery {
block(slot: ${defaultSlot}) {
blockTime
}
}
}
`;
`;
const result = await rpcGraphQL.query(source);
expect(result).toMatchObject({
data: {
Expand All @@ -55,25 +110,31 @@ describe('block', () => {
expect.assertions(1);
fetchMock.mockOnce(JSON.stringify(mockRpcResponse(mockBlockFull)));
const source = /* GraphQL */ `
query testQuery {
block(slot: ${defaultSlot}) {
blockhash
parentSlot
rewards {
pubkey
lamports
postBalance
rewardType
query testQuery {
block(slot: ${defaultSlot}) {
blockHeight
blockTime
blockhash
parentSlot
previousBlockhash
rewards {
pubkey
lamports
postBalance
rewardType
}
}
}
}
`;
`;
const result = await rpcGraphQL.query(source);
expect(result).toMatchObject({
data: {
block: {
blockHeight: expect.any(BigInt),
blockTime: expect.any(Number),
blockhash: expect.any(String),
parentSlot: expect.any(BigInt),
previousBlockhash: expect.any(String),
rewards: expect.arrayContaining([
{
lamports: expect.any(BigInt),
Expand All @@ -92,14 +153,14 @@ describe('block', () => {
expect.assertions(1);
fetchMock.mockOnce(JSON.stringify(mockRpcResponse(mockBlockSignatures)));
const source = /* GraphQL */ `
query testQuery {
block(slot: ${defaultSlot}, transactionDetails: signatures) {
... on BlockWithSignatures {
signatures
query testQuery {
block(slot: ${defaultSlot}, transactionDetails: signatures) {
... on BlockWithSignatures {
signatures
}
}
}
}
`;
`;
const result = await rpcGraphQL.query(source);
expect(result).toMatchObject({
data: {
Expand All @@ -115,19 +176,19 @@ describe('block', () => {
expect.assertions(1);
fetchMock.mockOnce(JSON.stringify(mockRpcResponse(mockBlockAccounts)));
const source = /* GraphQL */ `
query testQuery {
block(slot: ${defaultSlot}, transactionDetails: accounts) {
... on BlockWithAccounts {
transactions {
data {
accountKeys
signatures
query testQuery {
block(slot: ${defaultSlot}, transactionDetails: accounts) {
... on BlockWithAccounts {
transactions {
data {
accountKeys
signatures
}
}
}
}
}
}
`;
`;
const result = await rpcGraphQL.query(source);
expect(result).toMatchObject({
data: {
Expand All @@ -150,20 +211,20 @@ describe('block', () => {
expect.assertions(1);
fetchMock.mockOnce(JSON.stringify(mockRpcResponse(mockBlockNone)));
const source = /* GraphQL */ `
query testQuery {
block(slot: ${defaultSlot}, transactionDetails: none) {
... on BlockWithNone {
blockhash
rewards {
pubkey
lamports
postBalance
rewardType
query testQuery {
block(slot: ${defaultSlot}, transactionDetails: none) {
... on BlockWithNone {
blockhash
rewards {
pubkey
lamports
postBalance
rewardType
}
}
}
}
}
`;
`;
const result = await rpcGraphQL.query(source);
expect(result).toMatchObject({
data: {
Expand All @@ -187,18 +248,18 @@ describe('block', () => {
expect.assertions(1);
fetchMock.mockOnce(JSON.stringify(mockRpcResponse(mockBlockFullBase58)));
const source = /* GraphQL */ `
query testQuery {
block(slot: ${defaultSlot}, encoding: BASE_58) {
... on BlockWithFull {
transactions {
... on TransactionBase58 {
data
query testQuery {
block(slot: ${defaultSlot}, encoding: BASE_58) {
... on BlockWithFull {
transactions {
... on TransactionBase58 {
data
}
}
}
}
}
}
`;
`;
const result = await rpcGraphQL.query(source);
expect(result).toMatchObject({
data: {
Expand Down
6 changes: 2 additions & 4 deletions packages/rpc-graphql/src/loaders/account.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { SolanaRpcMethods } from '@solana/rpc-core';
import DataLoader from 'dataloader';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import fastStableStringify from 'fast-stable-stringify';
import { GraphQLResolveInfo } from 'graphql';

import type { Rpc } from '../context';
import { AccountQueryArgs } from '../schema/account';
import { cacheKeyFn } from './common/cache-key-fn';
import { onlyPresentFieldRequested } from './common/resolve-info';
import { transformLoadedAccount } from './transformers/account';

Expand Down Expand Up @@ -41,7 +39,7 @@ function createAccountBatchLoadFn(rpc: Rpc) {
}

export function createAccountLoader(rpc: Rpc) {
const loader = new DataLoader(createAccountBatchLoadFn(rpc), { cacheKeyFn: fastStableStringify });
const loader = new DataLoader(createAccountBatchLoadFn(rpc), { cacheKeyFn });
return {
load: async (args: AccountQueryArgs, info?: GraphQLResolveInfo) => {
if (onlyPresentFieldRequested('address', info)) {
Expand Down
6 changes: 2 additions & 4 deletions packages/rpc-graphql/src/loaders/block.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { SolanaRpcMethods } from '@solana/rpc-core';
import DataLoader from 'dataloader';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import fastStableStringify from 'fast-stable-stringify';
import { GraphQLResolveInfo } from 'graphql';

import type { Rpc } from '../context';
import { BlockQueryArgs } from '../schema/block';
import { cacheKeyFn } from './common/cache-key-fn';
import { onlyPresentFieldRequested } from './common/resolve-info';
import { transformLoadedBlock } from './transformers/block';

Expand Down Expand Up @@ -49,7 +47,7 @@ function createBlockBatchLoadFn(rpc: Rpc) {
}

export function createBlockLoader(rpc: Rpc) {
const loader = new DataLoader(createBlockBatchLoadFn(rpc), { cacheKeyFn: fastStableStringify });
const loader = new DataLoader(createBlockBatchLoadFn(rpc), { cacheKeyFn });
return {
load: async (args: BlockQueryArgs, info?: GraphQLResolveInfo) => {
if (onlyPresentFieldRequested('slot', info)) {
Expand Down
39 changes: 39 additions & 0 deletions packages/rpc-graphql/src/loaders/common/cache-key-fn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import fastStableStringify from 'fast-stable-stringify';

// This is a temporary fix for now. It is not optimized for performance,
// however, this library's parameter sets used for cache keys are relatively
// small, so it should not be a problem.
//
// `fast-stable-stringify` does not support `bigint` values, and some
// alternatives seem to opt for converting `bigint` to number. It's much more
// preferable, in the context of cache keys for relatively small parameter
// sets, to convert `bigint` to `string` instead.
//
// Although one might suggest setting the global `BigInt.prototype.toString`,
// however, `fast-stable-stringify` attempts to identify boolean, object,
// function, undefined, then string before defaulting to number, so there is no
// feasible way to inject `BigInt.prototype.toString` into the call stack.
//
// This function converts any `bigint` values in the object to `string` values,
// then passes the modified object to `fast-stable-stringify`.
export function cacheKeyFn(obj: any) {
function bigintToString(obj: any) {
if (typeof obj === 'bigint') {
return obj.toString();
}
if (typeof obj === 'object') {
const newObj = {};
for (const key in obj) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
newObj[key] = bigintToString(obj[key]);
}
return newObj;
}
return obj;
}
return fastStableStringify(bigintToString(obj));
}
6 changes: 2 additions & 4 deletions packages/rpc-graphql/src/loaders/program-accounts.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SolanaRpcMethods } from '@solana/rpc-core';
import DataLoader from 'dataloader';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import fastStableStringify from 'fast-stable-stringify';
import { GraphQLResolveInfo } from 'graphql';

import type { Rpc } from '../context';
import { ProgramAccountsQueryArgs } from '../schema/program-accounts';
import { cacheKeyFn } from './common/cache-key-fn';
import { onlyPresentFieldRequested } from './common/resolve-info';
import { transformLoadedAccount } from './transformers/account';

Expand Down Expand Up @@ -57,7 +55,7 @@ function createProgramAccountsBatchLoadFn(rpc: Rpc) {
}

export function createProgramAccountsLoader(rpc: Rpc) {
const loader = new DataLoader(createProgramAccountsBatchLoadFn(rpc), { cacheKeyFn: fastStableStringify });
const loader = new DataLoader(createProgramAccountsBatchLoadFn(rpc), { cacheKeyFn });
return {
load: async (args: ProgramAccountsQueryArgs, info?: GraphQLResolveInfo) => {
if (onlyPresentFieldRequested('programAddress', info)) {
Expand Down
6 changes: 2 additions & 4 deletions packages/rpc-graphql/src/loaders/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { SolanaRpcMethods } from '@solana/rpc-core';
import DataLoader from 'dataloader';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import fastStableStringify from 'fast-stable-stringify';
import { GraphQLResolveInfo } from 'graphql';

import type { Rpc } from '../context';
import { TransactionQueryArgs } from '../schema/transaction';
import { cacheKeyFn } from './common/cache-key-fn';
import { onlyPresentFieldRequested } from './common/resolve-info';
import { transformLoadedTransaction } from './transformers/transaction';

Expand Down Expand Up @@ -64,7 +62,7 @@ function createTransactionBatchLoadFn(rpc: Rpc) {
}

export function createTransactionLoader(rpc: Rpc) {
const loader = new DataLoader(createTransactionBatchLoadFn(rpc), { cacheKeyFn: fastStableStringify });
const loader = new DataLoader(createTransactionBatchLoadFn(rpc), { cacheKeyFn });
return {
load: async (args: TransactionQueryArgs, info?: GraphQLResolveInfo) => {
if (onlyPresentFieldRequested('signature', info)) {
Expand Down