Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions packages/rpc-core/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
1 change: 1 addition & 0 deletions packages/rpc-core/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
20 changes: 20 additions & 0 deletions packages/rpc-core/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2018 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.
99 changes: 99 additions & 0 deletions packages/rpc-core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"name": "@solana/rpc-core",
"version": "0.0.0-development",
"description": "A library for making calls to the Solana JSON RPC API",
"exports": {
"browser": {
"import": "./dist/index.browser.js",
"require": "./dist/index.browser.cjs"
},
"node": {
"import": "./dist/index.node.js",
"require": "./dist/index.node.cjs"
},
"react-native": "./dist/index.native.js",
"types": "./dist/types/index.d.ts"
},
"browser": {
"./dist/index.node.cjs": "./dist/index.browser.cjs",
"./dist/index.node.js": "./dist/index.browser.js"
},
"main": "./dist/index.node.cjs",
"module": "./dist/index.node.js",
"react-native": "./dist/index.native.js",
"types": "./dist/types/index.d.ts",
"type": "module",
"files": [
"./dist/"
],
"sideEffects": false,
"keywords": [
"blockchain",
"solana",
"web3"
],
"scripts": {
"compile:js": "tsup --config build-scripts/tsup.config.package.ts",
"compile:typedefs": "tsc -p ./tsconfig.declarations.json",
"dev": "jest -c node_modules/test-config/jest-dev.config.ts --rootDir . --watch",
"test:lint": "jest -c node_modules/test-config/jest-lint.config.ts --rootDir . --silent",
"test:prettier": "jest -c node_modules/test-config/jest-prettier.config.ts --rootDir . --silent",
"test:treeshakability:browser": "agadoo dist/index.browser.js",
"test:treeshakability:native": "agadoo dist/index.node.js",
"test:treeshakability:node": "agadoo dist/index.native.js",
"test:typecheck": "tsc --noEmit",
"test:unit:browser": "jest -c node_modules/test-config/jest-unit.config.browser.ts --rootDir . --silent",
"test:unit:node": "jest -c node_modules/test-config/jest-unit.config.node.ts --rootDir . --silent"
},
"author": "Solana Labs Maintainers <[email protected]>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/solana-labs/solana-web3.js"
},
"bugs": {
"url": "https://github.com/solana-labs/solana-web3.js/issues"
},
"browserslist": [
"supports bigint and not dead",
"maintained node versions"
],
"dependencies": {
"@solana/keys": "workspace:*",
"@solana/rpc-transport": "workspace:*"
},
"devDependencies": {
"@solana/eslint-config-solana": "^0.0.4",
"@swc/core": "^1.3.18",
"@swc/jest": "^0.2.23",
"@types/jest": "^29.2.3",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"agadoo": "^2.0.0",
"build-scripts": "workspace:*",
"eslint": "^8.27.0",
"eslint-plugin-jest": "^27.1.5",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-sort-keys-fix": "^1.1.2",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"jest-runner-eslint": "^1.1.0",
"jest-runner-prettier": "^1.0.0",
"postcss": "^8.4.12",
"prettier": "^2.7.1",
"test-config": "workspace:*",
"ts-node": "^10.9.1",
"tsconfig": "workspace:*",
"tsup": "6.5.0",
"turbo": "^1.6.3",
"typescript": "^4.9"
},
"bundlewatch": {
"defaultCompression": "gzip",
"files": [
{
"path": "./dist/index*.js"
}
]
}
}
26 changes: 26 additions & 0 deletions packages/rpc-core/src/__tests__/rpc-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { IJsonRpcTransport } from '@solana/rpc-transport';
import { rpc } from '../rpc';

describe('rpc', () => {
let transport: jest.Mocked<IJsonRpcTransport>;
beforeEach(() => {
transport = { send: jest.fn() };
});
describe('a method call without params', () => {
beforeEach(async () => {
await rpc.getBlockHeight(transport);
});
it('calls `send` on the supplied transport with the function name as the method name and `undefined` params', () => {
expect(transport.send).toHaveBeenCalledWith('getBlockHeight', undefined);
});
});
describe('a method call with params', () => {
const params = [1, undefined, { commitment: 'finalized' }] as const;
beforeEach(async () => {
await rpc.getBlocks(transport, ...params);
});
it('calls `send` on the supplied transport with the function name as the method name and the supplied params', () => {
expect(transport.send).toHaveBeenCalledWith('getBlocks', params);
});
});
});
1 change: 1 addition & 0 deletions packages/rpc-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './rpc';
22 changes: 22 additions & 0 deletions packages/rpc-core/src/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { IJsonRpcTransport } from '@solana/rpc-transport';
import { JsonRpcApi } from './types/jsonRpcApi';

export const rpc = /* #__PURE__ */ new Proxy<JsonRpcApi>({} as JsonRpcApi, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL what a Proxy is, very cool!

defineProperty() {
return false;
},
deleteProperty() {
return false;
},
get<TMethodName extends keyof JsonRpcApi>(target: JsonRpcApi, p: TMethodName) {
if (target[p] == null) {
const method = p.toString();
target[p] = async function (transport: IJsonRpcTransport, ...params: Parameters<JsonRpcApi[TMethodName]>) {
const normalizedParams = params.length ? params : undefined;
const result = await transport.send(method, normalizedParams);
return result;
} as unknown as JsonRpcApi[TMethodName];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Learning a lot today with ... as unknown as ... -- does this cause potential footguns for transport implementers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At that point, it's our job to make sure that the implementation of rpc.* is perfect. Essentially what we're saying is that the dynamic get returns a method that will definitely conform to JsonRpcApi[TMethodName] but the implementation on lines 14-18 does not, from Typescript's perspective.

src/rpc.ts:14:13 - error TS2322: Type '(transport: IJsonRpcTransport, ...params: Parameters<JsonRpcApi[TMethodName]>) => Promise<unknown>' is not assignable to type 'JsonRpcApi[TMethodName]'.
  Type '(transport: IJsonRpcTransport, ...params: Parameters<JsonRpcApi[TMethodName]>) => Promise<unknown>' is not assignable to type '{ (transport: IJsonRPCTransport, address: Base58EncodedAddress, config?: ({ encoding: "base64"; } & GetAccountInfoApiCommonConfig & GetAccountInfoApiBase64EncodingCommonConfig) | undefined): Promise<...>; (transport: IJsonRPCTransport, address: Base58EncodedAddress, config?: ({ ...; } & ... 1 more ... & GetAccountIn...'.
    Type '(transport: IJsonRpcTransport, ...params: Parameters<JsonRpcApi[TMethodName]>) => Promise<unknown>' is not assignable to type '{ (transport: IJsonRPCTransport, address: Base58EncodedAddress, config?: ({ encoding: "base64"; } & GetAccountInfoApiCommonConfig & GetAccountInfoApiBase64EncodingCommonConfig) | undefined): Promise<...>; (transport: IJsonRPCTransport, address: Base58EncodedAddress, config?: ({ ...; } & ... 1 more ... & GetAccountIn...'.
      Types of parameters 'params' and 'address' are incompatible.
        Type '[address: Base58EncodedAddress, config?: ({ encoding: "base64"; } & GetAccountInfoApiCommonConfig & GetAccountInfoApiBase64EncodingCommonConfig) | undefined]' is not assignable to type 'Parameters<JsonRpcApi[TMethodName]>'.

14             target[p] = async function (transport: IJsonRpcTransport, ...params: Parameters<JsonRpcApi[TMethodName]>) {
               ~~~~~~~~~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like these two should be compatible:

  • [address: Base58EncodedAddress, config?: ({ encoding: "base64"; } & GetAccountInfoApiCommonConfig & GetAccountInfoApiBase64EncodingCommonConfig) | undefined]
  • Parameters<JsonRpcApi[TMethodName]>

…but they aren't.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, that's really weird, thanks for explaining. Is that a typescript bug? Or does Parameters need to have some extra declaration to make that clear?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, but I think I made a mistake including transport in all the method signatures. I wonder what would happen if I took that out…

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
return target[p];
},
});
4 changes: 4 additions & 0 deletions packages/rpc-core/src/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare const __BROWSER__: boolean;
declare const __DEV__: boolean;
declare const __NODEJS__: boolean;
declare const __REACTNATIVE__: boolean;
5 changes: 5 additions & 0 deletions packages/rpc-core/src/types/jsonRpcApi.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { GetAccountInfoApi } from './rpc-methods/getAccountInfo';
import { GetBlockHeightApi } from './rpc-methods/getBlockHeight';
import { GetBlocksApi } from './rpc-methods/getBlocks';

declare interface JsonRpcApi extends GetAccountInfoApi, GetBlockHeightApi, GetBlocksApi {}
11 changes: 11 additions & 0 deletions packages/rpc-core/src/types/rpc-methods/common.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
declare type DataSlice = readonly {
offset: number;
length: number;
};

// TODO: Eventually move this into whatever package implements transactions
declare type Finality = 'confirmed' | 'finalized' | 'processed';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Is Finality the new term? If so, we should probably update the docs and all that. I'd prefer to keep this Commitment to make mental mapping easier, but I won't put up a big fight about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh. I thought these three were what we're calling finality, as opposed to the olde deprecated suite of commitments.

From web3.js today:

https://github.com/solana-labs/solana-web3.js/blob/ad23683e8a42c726995cf0c1f0f903b20152854f/packages/library-legacy/src/connection.ts#L476-L501

So yeah, I have this wrong, somehow. I'll rename it.


declare type Slot =
// TODO(solana-labs/solana/issues/30341) Represent as bigint
number;
Comment on lines +9 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we've talked about this before, so pardon asking it again here... with the BYO-Transport model, is it possible to require that transports properly handle u64s and make this a bigint?

Copy link
Contributor Author

@steveluscher steveluscher Mar 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, fast forward to when everything is bigint (or rewind, and have represented all u64s as JavaScript strings in the first place) and all that the transport has to do is to serialize bigints as string over the wire.

We can't do this today, because the Solana RPC doesn't accept them.

# params is normally [5, 10] but here I'm representing them as ["5", "10"]
curl https://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0", "id": 1,
    "method": "getBlocks",
    "params": ["5", "10"]
  }
'
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params: invalid type: string \"5\", expected u64."},"id":1}

We need to change the server to parse strings as u64 before we can do that on the client.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I see what you're saying. You're saying: make it a bigint now at the outer part of the interface, and have the transport (temporarily) downcast that to a JavaScript number, maybe optionally warning when it overflows. Is that right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering it if it would be possible at the transport level to use bigint everywhere, but I forgot that the problem is at the RPC level, and not web3.js. But yeah, since all of these interfaces are new, it might be worth doing everything right and use bigints everywhere.

92 changes: 92 additions & 0 deletions packages/rpc-core/src/types/rpc-methods/getAccountInfo.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Base58EncodedAddress } from '@solana/keys';
import { IJsonRPCTransport } from '../../rpc';

type Base64EncodedBytes = string & { readonly __base64EncodedBytes: unique symbol };
type Base64EncodedZStdCompressedBytes = string & { readonly __base64EncodedZStdCompressedBytes: unique symbol };

type Base64EncodedDataResponse = [Base64EncodedBytes, 'base64'];
type Base64EncodedZStdCompressedDataResponse = [Base64EncodedZStdCompressedBytes, 'base64+zstd'];

type GetAccountInfoApiResponseBase = Readonly<{
context: Readonly<{
slot: Slot;
}>;
value: Readonly<{
executable: boolean;
lamports: number; // TODO(solana-labs/solana/issues/30341) Represent as bigint
owner: Base64EncodedAddress;
rentEpoch: number; // TODO(solana-labs/solana/issues/30341) Represent as bigint
space: number; // TODO(solana-labs/solana/issues/30341) Represent as bigint
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm missing something, but in testing I didn't see space returned here. Here's a token account:

{
  "jsonrpc": "2.0",
  "result": {
    "context": {
      "apiVersion": "1.13.6",
      "slot": 181671824
    },
    "value": {
      "data": [
        "xvp6877brTo9ZfNqq8l0MbG75MLS9uDkfKYCA0UvXWEs07wIp7mh6Iz0YahgZDSe/Psf+oXDeVKNALXKNdFOFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
        "base64"
      ],
      "executable": false,
      "lamports": 2039280,
      "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      "rentEpoch": 0
    }
  },
  "id": 1
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha. In the docs, this is called size, in the docs' example it's called space, in the current web3.js implementation it's absent, and in the server implementation it's space and it's an Option<u64>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is only in v1.15.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah gotcha

}> | null;
}>;

type GetAccountInfoApiResponseWithEncodedData = Readonly<{
value: Readonly<{
data: Base64EncodedDataResponse;
}> | null;
}>;

type GetAccountInfoApiResponseWithEncodedZStdCompressedData = Readonly<{
value: Readonly<{
data: Base64EncodedZStdCompressedDataResponse;
}> | null;
}>;

type GetAccountInfoApiResponseWithJsonData = Readonly<{
value: Readonly<{
data:
| Readonly<{
// Name of the program that owns this account.
program: string;
parsed: unknown;
space: number; // TODO(solana-labs/solana/issues/30341) Represent as bigint
}>
// If `jsonParsed` encoding is requested but a parser cannot be found for the given
// account the `data` field falls back to `base64`.
| Base64EncodedDataResponse;
}> | null;
}>;

type GetAccountInfoApiCommonConfig = readonly {
// Defaults to `finalized`
commitment?: Finality;
// The minimum slot that the request can be evaluated at
minContextSlot?: Slot;
};

type GetAccountInfoApiBase64EncodingCommonConfig = readonly {
// Limit the returned account data using the provided "offset: <usize>" and "length: <usize>" fields.
dataSlice?: DataSlice;
};

declare interface GetAccountInfoApi {
/**
* Returns all information associated with the account of provided public key
*/
getAccountInfo(
transport: IJsonRPCTransport,
address: Base58EncodedAddress,
config?: readonly {
encoding: 'base64';
} &
GetAccountInfoApiCommonConfig &
GetAccountInfoApiBase64EncodingCommonConfig
): Promise<GetAccountInfoApiResponseBase & GetAccountInfoApiResponseWithEncodedData>;
getAccountInfo(
transport: IJsonRPCTransport,
address: Base58EncodedAddress,
config?: readonly {
encoding: 'base64+zstd';
} &
GetAccountInfoApiCommonConfig &
GetAccountInfoApiBase64EncodingCommonConfig
): Promise<GetAccountInfoApiResponseBase & GetAccountInfoApiResponseWithEncodedZStdCompressedData>;
getAccountInfo(
transport: IJsonRPCTransport,
address: Base58EncodedAddress,
config?: readonly {
encoding: 'jsonParsed';
} &
GetAccountInfoApiCommonConfig
): Promise<GetAccountInfoApiResponseBase & GetAccountInfoApiResponseWithJsonData>;
Comment on lines +66 to +91
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a much more complicated example, where you can see how modulating the inputs changes the output type (eg. specifying different encoding results in the output being different.

Things to pay attention to:

  • jsonParsed encoding results in data being a parsed data structure or [bytes, encoding] as a fallback if no parser is available.
  • only base64 encoding accepts dataSlice as an input

See https://docs.solana.com/api/http#getaccountinfo.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is extremely cool, and not nearly as complicated to read as I feared when I first opened the file 😅

}
20 changes: 20 additions & 0 deletions packages/rpc-core/src/types/rpc-methods/getBlockHeight.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IJsonRpcTransport } from '@solana/rpc-transport';

type GetBlockHeightApiResponse =
// TODO(solana-labs/solana/issues/30341) Represent as bigint
number;

declare interface GetBlockHeightApi {
/**
* Returns the current block height of the node
*/
getBlockHeight(
transport: IJsonRpcTransport,
config?: readonly {
// Defaults to `finalized`
commitment?: Finality;
// The minimum slot that the request can be evaluated at
minContextSlot?: Slot;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these levels of optionals... I can swee why graphql would make this so much neater

): Promise<GetBlockHeightApiResponse>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
18 changes: 18 additions & 0 deletions packages/rpc-core/src/types/rpc-methods/getBlocks.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { IJsonRpcTransport } from '@solana/rpc-transport';

type GetBlocksApiResponse = Slot[];

declare interface GetBlocksApi {
/**
* Returns a list of confirmed blocks between two slots
*/
getBlocks(
transport: IJsonRpcTransport,
startSlot: Slot,
endSlotInclusive?: Slot,
config?: readonly {
// Defaults to `finalized`
commitment?: Exclude<Finality, 'processed'>;
}
): Promise<GetBlocksApiResponse>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the docs are wrong on this one :-\

Copy link
Contributor Author

@steveluscher steveluscher Mar 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed them! I don't know why the change hasn't deployed yet. https://github.com/solana-labs/solana/pull/30351/files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yes. Anyway, thanks for fixing!

}
10 changes: 10 additions & 0 deletions packages/rpc-core/tsconfig.declarations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"outDir": "./dist/types"
},
"extends": "./tsconfig.json",
"include": ["src/index.ts", "src/types"]
}
9 changes: 9 additions & 0 deletions packages/rpc-core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"lib": ["ES2015.Proxy", "ES2015.Promise", "ES5"]
},
"display": "@solana/rpc-core",
"extends": "tsconfig/base.json",
"include": ["src"]
}