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

refactor(experimental): add RPC methods returning just a number #1271

Merged
merged 6 commits into from
Apr 25, 2023

Conversation

joncinque
Copy link
Contributor

@joncinque joncinque commented Apr 19, 2023

Summary

While doing one of the simple RPC calls, I noticed there's a bunch of simple RPC calls that take just a commitment with optional minContextSlot, or nothing at all, and return either a number, or a number wrapped in an rpc response. The calls covered here are:

  • getFirstAvailableBlock (take nothing, just give number)
  • getMaxRetransmitSlot (ditto)
  • getMaxShredInsertSlot (ditto)
  • getSlot (takes commitment and context, just gives number)
  • getStakeMinimumDelegation (takes ONLY commitment, wraps the number)
  • getTransactionCount (takes commitment and context, just gives number)
  • minimumLedgerSlot (takes nothing, just gives a number)

Since so many of the tests just ended up doing the same as the getBlockHeight test, I moved them all into the same file. Then I noticed that some of them take parameters and some don't, and still kept them in the same file. Then I noticed some return just a number, and others wrap it, and still I kept them in the same file.

So if you want these broken up differently, I'm totally game! Also, I used the each construction, which I'm not sure if that's best or if you prefer something else.

Test Plan

pnpm test

Also, I ran prettier by hand with pnpm prettier --write '{,{src,test}/**/}*.{j,t}s', I didn't find any other scripts or configuration, and it seemed to pass the prettier test.

@joncinque joncinque marked this pull request as draft April 20, 2023 00:01
@joncinque
Copy link
Contributor Author

Changing back to draft while I figure out the build error, sorry about that

@joncinque joncinque marked this pull request as ready for review April 20, 2023 00:20
@joncinque
Copy link
Contributor Author

Ok all good, just had to use Reflect.get in the tests rather than obj[func]

Copy link
Contributor

@steveluscher steveluscher left a comment

Choose a reason for hiding this comment

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

giphy

I do indeed prefer the ‘repetitive but separate’ approach to tests, because:

  • reading through all the interpolation and metaprogramming in this PR's test makes me think too hard
  • if (when) someone breaks the RPC implementation for one of these suckers it's going to show up in a test called ‘getBigInt’ which will again require thinking
  • some of these methods are actually different (some return context, and others a raw value) so you have logic in this file that doesn't actually apply to all.
  • You could imagine adding more special cases here which would make these tests even more dissimilar. Could you actually start the validator with a custom minimum stake delegation and then add a test for that? Might you write a test that actually lands a transaction and asserts that the value of getTransactionCount() increased?
  • when we add a new RPC method that only returns an integer, it will be really obvious that the author didn't add a test if there's literally no file for it, but less obvious if it's just missed here. A 1:1 correspondence between a method definition and a test file makes it easy to spot.

I was going to say ‘despite all that I don't feel super strongly about this,’ but the more bullet points I added the more I think… yeah maybe I do :)

@@ -0,0 +1,10 @@
import { RpcResponse, Slot } from './common';

type GetFirstAvailableBlockApiResponse = RpcResponse<Slot>;
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't return an RpcResponse in fact. It's just the raw integer!

// Not an `RpcResponse<TValue>`...
{context: {...}, value: 1234}
// Actually just `TValue`
1234
Suggested change
type GetFirstAvailableBlockApiResponse = RpcResponse<Slot>;
type GetFirstAvailableBlockApiResponse = Slot;

Copy link
Contributor Author

@joncinque joncinque Apr 20, 2023

Choose a reason for hiding this comment

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

Well at least I got it right in my PR description 😓 any idea about why this didn't get picked up by the compiler or the tests?

Edit: never mind, it makes sense that Reflect.get wouldn't be able to notice anything wrong since we're going around the compiler. Another point in favor of the separated tests!

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah! I mean we could write Typescript tests, but that’s pretty next level.

Copy link
Contributor

Choose a reason for hiding this comment

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

In case it wasn't clear what I meant by this, some people actually have written tests that assert whether source code raises type errors or not. Pretty extreme.

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 don't worry, it was very clear. I didn't want to go there under any circumstances 😅

packages/rpc-core/src/rpc-methods/getMaxRetransmitSlot.ts Outdated Show resolved Hide resolved
packages/rpc-core/src/rpc-methods/getMaxShredInsertSlot.ts Outdated Show resolved Hide resolved
packages/rpc-core/src/rpc-methods/getSlot.ts Outdated Show resolved Hide resolved
@@ -0,0 +1,14 @@
import { RpcResponse, U64UnsafeBeyond2Pow53Minus1, Commitment } from './common';

type GetStakeMinimumDelegationApiResponse = RpcResponse<U64UnsafeBeyond2Pow53Minus1>;
Copy link
Contributor

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.

I think at this point it's worth creating a branded Lamports type. See Base58EncodedAddress for how to create a branded type.

This will imply that you can create a function that only accepts Lamports as a type and if you try to pass it a non-branded BigInt you'll get a Typescript error. Should save a couple million in mistake fees.

Copy link
Contributor Author

@joncinque joncinque Apr 20, 2023

Choose a reason for hiding this comment

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

Yeah sure, where do you want it go? There's no clear place for it, so I'd probably just default to the top-level of rpc-core or in rpc-methods/common.ts but I'm not sure if it's an RPC-specific type

Copy link
Contributor

Choose a reason for hiding this comment

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

I have a @solana/keys package which is a nice place to put the address types, but for everything else I’ve just been hucking ‘primitive’ types into global typedefs.

Throw it anywhere for now, just try to avoid circular references between modules.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, put it under common.ts

@joncinque
Copy link
Contributor Author

Ok, this should be ready for another look!

@joncinque joncinque requested a review from steveluscher April 20, 2023 22:12
Copy link
Contributor

@steveluscher steveluscher left a comment

Choose a reason for hiding this comment

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

Thank you!!! I'll fix up my own nits and land this.

});
});
describe('when called with no parameters', () => {
it('returns a bigint', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess there are probably reasons why it's not safe to presume that the test validator will always return 0n here? I seem to remember that being a problem before: solana-labs/solana#23853

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similar to the stake delegation test, I didn't want this test to be in the business of validating the correctness of the response.

it('returns the result as a bigint wrapped in an RpcResponse', async () => {
expect.assertions(1);
const result = await rpc.getStakeMinimumDelegation({ commitment }).send();
expect(result.value).toEqual(expect.any(BigInt));
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we drop a TODO here to actually fetch the minimum delegation value from the stake program?

Probably not, now that I think about it. This test shouldn't really be in the business of testing the underlying method. It should stop short of testing the type of the responses, as you've done here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah exactly, that was my intention

@@ -19,3 +19,5 @@ export type RpcResponse<TValue> = Readonly<{
}>;
value: TValue;
}>;

export type Lamports = U64UnsafeBeyond2Pow53Minus1 & { readonly __lamports: unique symbol };
Copy link
Contributor

Choose a reason for hiding this comment

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

Mmmm… I think this is more like…

Suggested change
export type Lamports = U64UnsafeBeyond2Pow53Minus1 & { readonly __lamports: unique symbol };
export type LamportsUnsafeBeyond2Pow53Minus1 = bigint & { readonly __lamports: unique symbol };

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As you wish! In that case, do you also want to do SlotUnsafeBeyond2Pow53Minus1?

Copy link
Contributor

Choose a reason for hiding this comment

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

I asked how long it would take to get there and @willhickey told me ~114M years, so I think I decided against it.

image

Copy link
Contributor

Choose a reason for hiding this comment

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

Not opposed to it, for completeness, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah gotcha, that definitely makes sense

* Returns the lowest slot that the node has information about in its ledger.
* This value may increase over time if the node is configured to purge older ledger data.
*/
minimumLedgerSlot(): MinimumLedgerSlotApiResponse;
Copy link
Contributor

Choose a reason for hiding this comment

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

LOL @ no get.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same 😄

@steveluscher steveluscher merged commit ed7c7b8 into solana-labs:master Apr 25, 2023
@joncinque joncinque deleted the expu64 branch April 25, 2023 21:21
@github-actions
Copy link
Contributor

github-actions bot commented May 9, 2023

🎉 This PR is included in version 1.76.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions
Copy link
Contributor

Because there has been no activity on this PR for 14 days since it was merged, it has been automatically locked. Please open a new issue if it requires a follow up.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 24, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants