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

Commit

Permalink
An example that demonstrates how to add a custom JSON RPC API method …
Browse files Browse the repository at this point in the history
…to the base ones (#2985)

# Summary

Some JSON RPC servers implement additional methods, above and beyond the ones offered by the Solana JSON RPC API. We want developers to be able to mix these into their RPC instance commensurate with their RPC providers' capabilities.

In this PR we provide an example of how one might add a custom JSON RPC API method based on the `getAsset` method available on the public Solana Mainnet RPC server.

# Test Plan

```
cd examples/rpc-custom-api/
pnpm start

> @solana/example-rpc-custom-api@ start /home/sol/src/solana-web3.js-git/examples/rpc-custom-api
> tsx src/example.ts

INFO (Custom JSON RPC API): [step 1] Solana RPC methods like `getLatestBlockhash` still work
    blockhash: "C8Tqp653ZcyRcnPjnrwjRLfZBpwWPjSTFa5Fr8jA2iSL"
    lastValidBlockHeight: 258841174
INFO (Custom JSON RPC API): [step 2] The custom `getAssetMetadata` that we implemented also works
    metadata: {
      "name": "USD Coin",
      "symbol": "USDC"
    }
```
  • Loading branch information
steveluscher authored Jul 30, 2024
1 parent f42f49c commit 209a9a0
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 3 deletions.
20 changes: 20 additions & 0 deletions examples/rpc-custom-api/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2023 Solana Labs, Inc

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 changes: 20 additions & 0 deletions examples/rpc-custom-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@solana/example-rpc-custom-api",
"private": true,
"type": "module",
"scripts": {
"prestart": "turbo --output-logs=errors-only compile:js compile:typedefs",
"start": "tsx src/example.ts",
"style:fix": "pnpm eslint --fix src/* && pnpm prettier --log-level warn --ignore-unknown --write ./*",
"test:lint": "TERM_OVERRIDE=\"${TURBO_HASH:+dumb}\" TERM=${TERM_OVERRIDE:-$TERM} jest -c ../../node_modules/@solana/test-config/jest-lint.config.ts --rootDir . --silent --testMatch '<rootDir>src/**/*.{ts,tsx}'",
"test:prettier": "TERM_OVERRIDE=\"${TURBO_HASH:+dumb}\" TERM=${TERM_OVERRIDE:-$TERM} jest -c ../../node_modules/@solana/test-config/jest-prettier.config.ts --rootDir . --silent",
"test:typecheck": "tsc"
},
"dependencies": {
"@solana/example-utils": "workspace:*",
"@solana/web3.js": "workspace:@solana/web3.js-experimental@*"
},
"devDependencies": {
"tsx": "^4.16.2"
}
}
116 changes: 116 additions & 0 deletions examples/rpc-custom-api/src/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* EXAMPLE
* Add a custom JSON RPC method to the base Solana JSON RPC API using @solana/web3.js.
*
* To run this example, execute `pnpm start` in this directory.
*/
import { createLogger } from '@solana/example-utils/createLogger.js';
import {
Address,
address,
createDefaultRpcTransport,
createRpc,
createSolanaRpcApi,
DEFAULT_RPC_CONFIG,
mainnet,
RpcApi,
RpcApiMethods,
SolanaRpcApiMainnet,
} from '@solana/web3.js';

const log = createLogger('Custom JSON RPC API');

/**
* STEP 1: CUSTOM JSON RPC API CALL SIGNATURE
* Define the call signature of the custom API. For this example we will use the Triton One
* `getAsset` API, available on the public mainnet RPC server.
* https://docs.triton.one/digital-assets-api/metaplex-digital-assets-api/get-asset
*/
type AssetMetadata = Readonly<{
description: string;
name: string;
symbol: string;
}>;
interface TritonGetAssetApi extends RpcApiMethods {
/**
* Define the ideal developer-facing API as a TypeScript type. Doing so will enable typechecking
* and autocompletion for it on the RPC instance.
*/
getAssetMetadata(address: Address): AssetMetadata;
}

/**
* STEP 2: CUSTOM JSON RPC API IMPLEMENTATION
* Create an instance of the default JSON RPC API, then create a wrapper around it to intercept
* calls for custom API methods. The wrapper should format the inputs to satisfy the API of the JSON
* RPC server, and post-process the server response to satisfy the call signature defined above.
*/
const solanaRpcApi = createSolanaRpcApi<SolanaRpcApiMainnet>(DEFAULT_RPC_CONFIG);
/**
* Create a proxy that wraps the Solana RPC API and adds extra functionality.
*/
const customizedRpcApi = new Proxy(solanaRpcApi, {
defineProperty() {
return false;
},
deleteProperty() {
return false;
},
get(target, p, receiver) {
const methodName = p.toString();
if (methodName === 'getAssetMetadata') {
/**
* When the `getAssetMetadata` method is called on the RPC, return a custom definition.
*/
return (address: Address) => {
return {
/**
* If the JSON RPC API method is named differently than the method exposed on
* the custom API, supply it here.
*/
methodName: 'getAsset',
/**
* When the params that the JSON RPC API expects are formatted differently than
* the arguments to the method exposed on the custom API, reformat them here.
*/
params: { id: address },
/**
* When the return type of the method exposed on the custom API has a different
* shape than the result returned from the JSON RPC API, supply a transform.
*/
responseTransformer(rawResponse: { result: { content: { metadata: AssetMetadata } } }) {
return rawResponse.result.content.metadata;
},
};
};
} else {
/**
* If the method called is not a custom one, delegate to the original implementation.
*/
return Reflect.get(target, p, receiver);
}
},
}) as RpcApi<SolanaRpcApiMainnet & TritonGetAssetApi>; // Cast to a type that is a mix of both APIs.

/**
* STEP 3: RPC CONNECTION
* Combine the custom RPC API with a default JSON RPC transport to create an RPC instance.
*/
const customizedRpc = createRpc({
api: customizedRpcApi,
transport: createDefaultRpcTransport({ url: mainnet('https://api.mainnet-beta.solana.com') }),
});

/**
* STEP 4: USE THE DEFAULT API
* Test that the base API still works by calling a Solana RPC API method like `getLatestBlockhash`.
*/
const { value: latestBlockhash } = await customizedRpc.getLatestBlockhash().send();
log.info(latestBlockhash, '[step 1] Solana RPC methods like `getLatestBlockhash` still work');

/**
* STEP 5: USE THE CUSTOM API
* Test the custom `getAssetMetadata` method.
*/
const metadata = await customizedRpc.getAssetMetadata(address('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')).send();
log.info({ metadata }, '[step 2] The custom `getAssetMetadata` that we implemented also works');
12 changes: 12 additions & 0 deletions examples/rpc-custom-api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noEmit": true,
"target": "ESNext"
},
"display": "@solana/example-rpc-custom-api",
"extends": "../../packages/tsconfig/base.json",
"include": ["src"]
}
1 change: 1 addition & 0 deletions packages/rpc-subscriptions/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from '@solana/rpc-subscriptions-api';
export * from '@solana/rpc-subscriptions-spec';

export * from './rpc-default-config';
export * from './rpc-subscriptions';
export * from './rpc-subscriptions-clusters';
export * from './rpc-subscriptions-transport';
4 changes: 3 additions & 1 deletion packages/rpc-subscriptions/src/rpc-default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import type { createSolanaRpcSubscriptionsApi } from '@solana/rpc-subscriptions-

import { createSolanaJsonRpcIntegerOverflowError } from './rpc-integer-overflow-error';

export const DEFAULT_RPC_CONFIG: Partial<NonNullable<Parameters<typeof createSolanaRpcSubscriptionsApi>[0]>> = {
export const DEFAULT_RPC_SUBSCRIPTIONS_CONFIG: Partial<
NonNullable<Parameters<typeof createSolanaRpcSubscriptionsApi>[0]>
> = {
defaultCommitment: 'confirmed',
onIntegerOverflow(methodName, keyPath, value) {
throw createSolanaJsonRpcIntegerOverflowError(methodName, keyPath, value);
Expand Down
4 changes: 2 additions & 2 deletions packages/rpc-subscriptions/src/rpc-subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@solana/rpc-subscriptions-spec';
import { ClusterUrl } from '@solana/rpc-types';

import { DEFAULT_RPC_CONFIG } from './rpc-default-config';
import { DEFAULT_RPC_SUBSCRIPTIONS_CONFIG } from './rpc-default-config';
import type { RpcSubscriptionsFromTransport } from './rpc-subscriptions-clusters';
import { getRpcSubscriptionsWithSubscriptionCoalescing } from './rpc-subscriptions-coalescer';
import {
Expand Down Expand Up @@ -41,7 +41,7 @@ export function createSolanaRpcSubscriptionsFromTransport<
>(transport: TTransport) {
return pipe(
createSubscriptionRpc({
api: createSolanaRpcSubscriptionsApi<TApi>(DEFAULT_RPC_CONFIG),
api: createSolanaRpcSubscriptionsApi<TApi>(DEFAULT_RPC_SUBSCRIPTIONS_CONFIG),
transport,
}),
rpcSubscriptions =>
Expand Down
1 change: 1 addition & 0 deletions packages/rpc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from '@solana/rpc-api';
export * from '@solana/rpc-spec';

export * from './rpc';
export * from './rpc-default-config';
export * from './rpc-clusters';
export * from './rpc-transport';
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 209a9a0

Please sign in to comment.