-
Notifications
You must be signed in to change notification settings - Fork 1k
[experimental] A types-only RPC implementation using JavaScript proxies #1190
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
Changes from 4 commits
0bc8528
e3f892d
b2a8a82
b87de1a
1cd7604
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| dist/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| dist/ |
| 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. |
| 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" | ||
| } | ||
| ] | ||
| } | ||
| } |
| 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); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './rpc'; |
| 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, { | ||
| 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]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Learning a lot today with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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]>) {
~~~~~~~~~
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like these two should be compatible:
…but they aren't.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure, but I think I made a mistake including
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| return target[p]; | ||
| }, | ||
| }); | ||
| 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; |
| 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 {} |
| 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'; | ||
|
||
|
|
||
| declare type Slot = | ||
| // TODO(solana-labs/solana/issues/30341) Represent as bigint | ||
| number; | ||
|
Comment on lines
+9
to
+11
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, fast forward to when everything is 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I love this!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I'm missing something, but in testing I didn't see
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Haha. In the docs, this is called
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this is only in v1.15.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Things to pay attention to:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 😅 |
||
| } | ||
| 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; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| 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>; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like the docs are wrong on this one :-\
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah yes. Anyway, thanks for fixing! |
||
| } | ||
| 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"] | ||
| } |
| 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"] | ||
| } |
There was a problem hiding this comment.
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!