Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GraphQL]: Enable schema extension #2614

Open
4 tasks
buffalojoec opened this issue May 4, 2024 · 0 comments
Open
4 tasks

[GraphQL]: Enable schema extension #2614

buffalojoec opened this issue May 4, 2024 · 0 comments
Labels
enhancement New feature or request GraphQL
Milestone

Comments

@buffalojoec
Copy link
Collaborator

Motivation

Many projects who wish to use the RPC-GraphQL resolver library may wish to
extend the default schema, which is designed around the
Solana JSON-RPC API. Currently, the library does
not allow for such customization.

The library should support, at least, the following extension capabilities.

  • Custom types and type resolvers.
  • Custom root queries and query resolvers.
  • Account decoders for fetched encoded accounts.
  • Instruction decoders for fetched encoded transaction instructions.

Example use case

An example use case for the first two items can be defined as follows, where
a developer wishes to extend the schema to support querying of a custom
NftMasterEdition object.

const masterEditionAddress = 'B2Srva38aD8bWpjghkU7jKFUqT1Y4KB2ejAnsJbP2ibA';

// Define custom type definitions for the GraphQL schema.
const customTypeDefs = /* GraphQL */ `
    # A Solana Master Edition NFT.
    type NftMasterEdition {
        address: Address
        metadata: Account
        mint: Account
    }

    # Query to retrieve a Solana Master Edition NFT.
    type Query {
        masterEdition(address: Address!): NftMasterEdition
    }
`;

// Define custom resolvers for the GraphQL schema.
const customTypeResolvers = {
    // Resolver for the custom `NftMasterEdition` type.
    NftMasterEdition: {
        metadata: resolveAccount('metadata'),
        mint: resolveAccount('mint'),
    },
};

// Define custom queries for the GraphQL schema.
const customQueryResolvers = {
    // Query to retrieve a Solana Master Edition NFT.
    masterEdition: () => {
        return {
            // Arbitrary address.
            address: masterEditionAddress,
            // See scripts/fixtures/gpa1.json.
            metadata: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
            // See scripts/fixtures/spl-token-mint-account.json.
            mint: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr',
        };
    },
};

// Create the RPC-GraphQL client with the custom type definitions and
// resolvers.
const rpcGraphQL = createRpcGraphQL(rpc, {
    queryResolvers: customQueryResolvers,
    typeDefs: customTypeDefs,
    typeResolvers: customTypeResolvers,
});

// Create a test query for the custom `masterEdition` query.
const source = /* GraphQL */ `
    query ($masterEditionAddress: Address!) {
        masterEdition(address: $masterEditionAddress) {
            address
            metadata {
                address
                lamports
            }
            mint {
                address
                lamports
                ownerProgram {
                    address
                }
                ... on MintAccount {
                    decimals
                    supply
                }
            }
        }
    }
`;

// Execute the test query.
const result = await rpcGraphQL.query(source, {
    masterEditionAddress,
});

Another example use case for account decoding can be defined as follows, using the
same custom NftMasterEdition type. Note that this same concept can also be applied
to decoding transaction instructions.

const masterEditionAddress = 'B2Srva38aD8bWpjghkU7jKFUqT1Y4KB2ejAnsJbP2ibA';

// Define custom type definitions for the GraphQL schema.
const customTypeDefs = /* GraphQL */ `
    # A Solana Master Edition NFT.
    type NftMasterEdition implements Account {
        address: Address
        data(encoding: AccountEncoding!, dataSlice: DataSlice): String
        executable: Boolean
        lamports: BigInt
        ownerProgram: Account
        space: BigInt
        rentEpoch: BigInt
        metadata: Account
        mint: Account
    }
`;

// Define custom resolvers for the GraphQL schema.
const customTypeResolvers = {
    // Resolver for the custom `NftMasterEdition` account type.
    NftMasterEdition: {
        metadata: resolveAccount('metadata'),
        mint: resolveAccount('mint'),
    },
};

// Create the RPC-GraphQL client with the custom type definitions and
// resolvers.
const rpcGraphQL = createRpcGraphQL(rpc, {
    typeDefs: customTypeDefs,
    typeResolvers: customTypeResolvers,
});

// Create a test query for the custom `masterEdition` query.
const source = /* GraphQL */ `
    query ($masterEditionAddress: Address!) {
        account(address: $masterEditionAddress) {
            address
            lamports
            ... on NftMasterEdition {
                metadata {
                    address
                    lamports
                }
                mint {
                    address
                    lamports
                    ownerProgram {
                        address
                    }
                    ... on MintAccount {
                        decimals
                        supply
                    }
                }
            }
        }
    }
`;

// Execute the test query.
const result = await rpcGraphQL.query(source, {
    masterEditionAddress,
});

Details

The first example demonstrated above is quite trivial. The resolver library needs
only minimal changes to support new queries, resolvers, and types. However, the
second example is trickier to implement.

When implementing custom account and instruction type support, one must consider
the ramifications of how serialization should be conducted. Specifically, how does the
resolver know which account type to attempt to deserialize from some base64
encoded data? Imagine the developer has provided 30 types of accounts (say, from
multiple IDLs).

It's impractical to simply attempt to deserialize an encoded account's data until one
is successful. Not only is this computationally intensive, but it's also prone to bugs
(imagine two or more account type layouts are highly similar and don't use discriminators).
For this reason, the decision-making aspect of telling RPC-GraphQL how and when to
deserialize account data should be left up to the developer to implement. This way, they
can take into account the use of discriminators or other optimizations for decoding data,
without compromising performance on the base library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request GraphQL
Projects
None yet
Development

No branches or pull requests

1 participant