diff --git a/examples/rpc-custom-api/LICENSE b/examples/rpc-custom-api/LICENSE new file mode 100644 index 000000000000..ec09953d3c23 --- /dev/null +++ b/examples/rpc-custom-api/LICENSE @@ -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. diff --git a/examples/rpc-custom-api/package.json b/examples/rpc-custom-api/package.json new file mode 100644 index 000000000000..24328da925f9 --- /dev/null +++ b/examples/rpc-custom-api/package.json @@ -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 '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" + } +} diff --git a/examples/rpc-custom-api/src/example.ts b/examples/rpc-custom-api/src/example.ts new file mode 100644 index 000000000000..d54573490818 --- /dev/null +++ b/examples/rpc-custom-api/src/example.ts @@ -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(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; // 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'); diff --git a/examples/rpc-custom-api/tsconfig.json b/examples/rpc-custom-api/tsconfig.json new file mode 100644 index 000000000000..6c95b8a8d415 --- /dev/null +++ b/examples/rpc-custom-api/tsconfig.json @@ -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"] +} diff --git a/packages/rpc-subscriptions/src/index.ts b/packages/rpc-subscriptions/src/index.ts index 072b24a6d78e..1c8a70849faa 100644 --- a/packages/rpc-subscriptions/src/index.ts +++ b/packages/rpc-subscriptions/src/index.ts @@ -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'; diff --git a/packages/rpc-subscriptions/src/rpc-default-config.ts b/packages/rpc-subscriptions/src/rpc-default-config.ts index cc035415ce1a..490889e8e682 100644 --- a/packages/rpc-subscriptions/src/rpc-default-config.ts +++ b/packages/rpc-subscriptions/src/rpc-default-config.ts @@ -2,7 +2,9 @@ import type { createSolanaRpcSubscriptionsApi } from '@solana/rpc-subscriptions- import { createSolanaJsonRpcIntegerOverflowError } from './rpc-integer-overflow-error'; -export const DEFAULT_RPC_CONFIG: Partial[0]>> = { +export const DEFAULT_RPC_SUBSCRIPTIONS_CONFIG: Partial< + NonNullable[0]> +> = { defaultCommitment: 'confirmed', onIntegerOverflow(methodName, keyPath, value) { throw createSolanaJsonRpcIntegerOverflowError(methodName, keyPath, value); diff --git a/packages/rpc-subscriptions/src/rpc-subscriptions.ts b/packages/rpc-subscriptions/src/rpc-subscriptions.ts index 5aa659569537..0b4023a5f8ef 100644 --- a/packages/rpc-subscriptions/src/rpc-subscriptions.ts +++ b/packages/rpc-subscriptions/src/rpc-subscriptions.ts @@ -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 { @@ -41,7 +41,7 @@ export function createSolanaRpcSubscriptionsFromTransport< >(transport: TTransport) { return pipe( createSubscriptionRpc({ - api: createSolanaRpcSubscriptionsApi(DEFAULT_RPC_CONFIG), + api: createSolanaRpcSubscriptionsApi(DEFAULT_RPC_SUBSCRIPTIONS_CONFIG), transport, }), rpcSubscriptions => diff --git a/packages/rpc/src/index.ts b/packages/rpc/src/index.ts index a53cabe6c4d4..26b7f47639d9 100644 --- a/packages/rpc/src/index.ts +++ b/packages/rpc/src/index.ts @@ -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'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c20f951a68ec..41ed3088349b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,6 +200,19 @@ importers: specifier: ^5.3.5 version: 5.3.5(@types/node@22.0.0)(terser@5.18.0) + examples/rpc-custom-api: + dependencies: + '@solana/example-utils': + specifier: workspace:* + version: link:../utils + '@solana/web3.js': + specifier: workspace:@solana/web3.js-experimental@* + version: link:../../packages/library + devDependencies: + tsx: + specifier: ^4.16.2 + version: 4.16.2 + examples/rpc-transport-throttled: dependencies: '@solana/example-utils':