Skip to content

fix: avoid treating JS protocol hooks as RPC methods#1748

Open
ChargingFoxSec wants to merge 3 commits into
anza-xyz:mainfrom
ChargingFoxSec:fix/rpc-proxy-js-protocol-hooks-509
Open

fix: avoid treating JS protocol hooks as RPC methods#1748
ChargingFoxSec wants to merge 3 commits into
anza-xyz:mainfrom
ChargingFoxSec:fix/rpc-proxy-js-protocol-hooks-509

Conversation

@ChargingFoxSec

Copy link
Copy Markdown
Contributor

Problem

RPC and subscription proxy objects currently treat most property access as an RPC method name. After #508, then is handled specially, but other JavaScript protocol hooks can still be interpreted as methods.

For example, JSON.stringify(rpc) looks up toJSON, and primitive/iteration/inspection paths can look up symbols such as Symbol.toPrimitive and Symbol.iterator.

Summary of Changes

Fixes #509.

This adds a narrow non-RPC property check for RPC proxy objects and subscription proxy objects.

  • Avoid treating JavaScript protocol hooks like toJSON, Symbol.toPrimitive, Symbol.iterator, Symbol.asyncIterator, Symbol.toStringTag, and Node's custom inspect symbol as RPC methods.
  • Preserve normal Object prototype behavior for properties such as toString and hasOwnProperty.
  • Apply the behavior to both outer RPC objects and the lower-level API proxy objects.
  • Add regression tests for RPC and subscription proxies.

Testing

  • corepack pnpm --filter @solana/rpc-spec test:unit:node
  • corepack pnpm --filter @solana/rpc-subscriptions-spec test:unit:node
  • corepack pnpm --filter @solana/rpc-spec test:unit:browser
  • corepack pnpm --filter @solana/rpc-subscriptions-spec test:unit:browser
  • corepack pnpm --filter @solana/rpc-spec test:typecheck
  • corepack pnpm --filter @solana/rpc-subscriptions-spec test:typecheck
  • corepack pnpm --filter @solana/rpc-spec test:lint
  • corepack pnpm --filter @solana/rpc-subscriptions-spec test:lint
  • corepack pnpm --filter @solana/rpc-spec test:prettier
  • corepack pnpm --filter @solana/rpc-subscriptions-spec test:prettier
  • corepack pnpm --filter @solana/rpc-spec compile:js
  • corepack pnpm --filter @solana/rpc-subscriptions-spec compile:js

@changeset-bot

changeset-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 19d28b6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 47 packages
Name Type
@solana/rpc-spec Patch
@solana/rpc-spec-types Patch
@solana/rpc-subscriptions-spec Patch
@solana/accounts Patch
@solana/plugin-interfaces Patch
@solana/rpc-api Patch
@solana/rpc-transport-http Patch
@solana/rpc Patch
@solana/sysvars Patch
@solana/kit Patch
@solana/rpc-subscriptions Patch
@solana/rpc-transformers Patch
@solana/rpc-subscriptions-api Patch
@solana/rpc-subscriptions-channel-websocket Patch
@solana/program-client-core Patch
@solana/rpc-graphql Patch
@solana/transaction-confirmation Patch
@solana/addresses Patch
@solana/assertions Patch
@solana/codecs-core Patch
@solana/codecs-data-structures Patch
@solana/codecs-numbers Patch
@solana/codecs-strings Patch
@solana/codecs Patch
@solana/compat Patch
@solana/errors Patch
@solana/fast-stable-stringify Patch
@solana/fixed-points Patch
@solana/functional Patch
@solana/instruction-plans Patch
@solana/instructions Patch
@solana/keys Patch
@solana/nominal-types Patch
@solana/offchain-messages Patch
@solana/options Patch
@solana/plugin-core Patch
@solana/programs Patch
@solana/promises Patch
@solana/react Patch
@solana/rpc-parsed-types Patch
@solana/rpc-types Patch
@solana/signers Patch
@solana/subscribable Patch
@solana/transaction-messages Patch
@solana/transactions Patch
@solana/wallet-account-signer Patch
@solana/webcrypto-ed25519-polyfill Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@bundlemon

bundlemon Bot commented Jun 24, 2026

Copy link
Copy Markdown

BundleMon

Files updated (25)
Status Path Size Limits
rpc-spec-types/dist/index.native.mjs
1.54KB (+403B +34.3%) -
rpc-spec-types/dist/index.node.mjs
1.54KB (+403B +34.33%) -
rpc-spec-types/dist/index.browser.mjs
1.54KB (+402B +34.15%) -
rpc-spec/dist/index.native.mjs
928B (+31B +3.46%) -
rpc-spec/dist/index.browser.mjs
928B (+30B +3.34%) -
rpc-spec/dist/index.node.mjs
926B (+30B +3.35%) -
rpc-subscriptions-spec/dist/index.browser.mjs
2.21KB (+24B +1.07%) -
rpc-subscriptions-spec/dist/index.native.mjs
2.21KB (+24B +1.07%) -
rpc-subscriptions-spec/dist/index.node.mjs
2.26KB (+24B +1.05%) -
transaction-introspection/dist/index.browser.
mjs
2.75KB (+20B +0.71%) -
transaction-introspection/dist/index.native.m
js
2.75KB (+19B +0.68%) -
transaction-introspection/dist/index.node.mjs
2.75KB (+19B +0.68%) -
wallet-account-signer/dist/index.browser.mjs
18.26KB (-33B -0.18%) -
wallet-account-signer/dist/index.native.mjs
18.26KB (-33B -0.18%) -
wallet-account-signer/dist/index.node.mjs
18.28KB (-33B -0.18%) -
rpc-api/dist/index.browser.mjs
1015B (-48B -4.52%) -
rpc-api/dist/index.native.mjs
1014B (-48B -4.52%) -
rpc-api/dist/index.node.mjs
1012B (-48B -4.53%) -
errors/dist/index.node.mjs
21.55KB (-52B -0.24%) -
errors/dist/index.browser.mjs
21.53KB (-53B -0.24%) -
errors/dist/index.native.mjs
21.52KB (-53B -0.24%) -
@solana/kit production bundle
kit/dist/index.production.min.js
55.13KB (-129B -0.23%) -
subscribable/dist/index.browser.mjs
2.73KB (-333B -10.66%) -
subscribable/dist/index.native.mjs
2.73KB (-334B -10.67%) -
subscribable/dist/index.node.mjs
2.8KB (-334B -10.43%) -
Unchanged files (125)
Status Path Size Limits
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.82KB -
rpc-graphql/dist/index.node.mjs
18.82KB -
transaction-messages/dist/index.browser.mjs
11.34KB -
transaction-messages/dist/index.native.mjs
11.34KB -
transaction-messages/dist/index.node.mjs
11.34KB -
instruction-plans/dist/index.browser.mjs
7.02KB -
instruction-plans/dist/index.native.mjs
7.02KB -
instruction-plans/dist/index.node.mjs
7.02KB -
codecs-data-structures/dist/index.browser.mjs
5.3KB -
codecs-data-structures/dist/index.native.mjs
5.3KB -
codecs-data-structures/dist/index.node.mjs
5.29KB -
fixed-points/dist/index.browser.mjs
5.08KB -
fixed-points/dist/index.native.mjs
5.07KB -
fixed-points/dist/index.node.mjs
5.07KB -
offchain-messages/dist/index.browser.mjs
5.06KB -
offchain-messages/dist/index.native.mjs
5.06KB -
offchain-messages/dist/index.node.mjs
5.06KB -
react/dist/index.browser.mjs
5.02KB -
react/dist/index.node.mjs
5.02KB -
react/dist/index.native.mjs
5.02KB -
kit/dist/index.browser.mjs
4.6KB -
kit/dist/index.native.mjs
4.6KB -
kit/dist/index.node.mjs
4.6KB -
transactions/dist/index.browser.mjs
4.07KB -
transactions/dist/index.native.mjs
4.07KB -
transactions/dist/index.node.mjs
4.07KB -
codecs-core/dist/index.browser.mjs
3.62KB -
codecs-core/dist/index.native.mjs
3.62KB -
codecs-core/dist/index.node.mjs
3.62KB -
webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.61KB -
webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.59KB -
webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.57KB -
rpc-subscriptions/dist/index.browser.mjs
3.37KB -
rpc-subscriptions/dist/index.node.mjs
3.34KB -
rpc-subscriptions/dist/index.native.mjs
3.31KB -
signers/dist/index.browser.mjs
3.26KB -
signers/dist/index.native.mjs
3.26KB -
signers/dist/index.node.mjs
3.26KB -
rpc-transformers/dist/index.browser.mjs
3.16KB -
rpc-transformers/dist/index.native.mjs
3.16KB -
rpc-transformers/dist/index.node.mjs
3.15KB -
keys/dist/index.node.mjs
3.06KB -
addresses/dist/index.browser.mjs
2.93KB -
addresses/dist/index.native.mjs
2.92KB -
addresses/dist/index.node.mjs
2.92KB -
keys/dist/index.browser.mjs
2.85KB -
keys/dist/index.native.mjs
2.85KB -
codecs-strings/dist/index.browser.mjs
2.55KB -
codecs-strings/dist/index.node.mjs
2.51KB -
codecs-strings/dist/index.native.mjs
2.47KB -
transaction-confirmation/dist/index.node.mjs
2.42KB -
transaction-confirmation/dist/index.native.mj
s
2.37KB -
sysvars/dist/index.browser.mjs
2.37KB -
sysvars/dist/index.native.mjs
2.37KB -
transaction-confirmation/dist/index.browser.m
js
2.37KB -
sysvars/dist/index.node.mjs
2.37KB -
rpc/dist/index.node.mjs
1.95KB -
codecs-numbers/dist/index.browser.mjs
1.95KB -
codecs-numbers/dist/index.native.mjs
1.95KB -
codecs-numbers/dist/index.node.mjs
1.94KB -
rpc-types/dist/index.browser.mjs
1.9KB -
rpc-types/dist/index.native.mjs
1.9KB -
rpc-types/dist/index.node.mjs
1.9KB -
rpc-transport-http/dist/index.browser.mjs
1.89KB -
rpc-transport-http/dist/index.native.mjs
1.89KB -
rpc/dist/index.native.mjs
1.81KB -
rpc/dist/index.browser.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.71KB -
rpc-subscriptions-channel-websocket/dist/inde
x.node.mjs
1.33KB -
rpc-subscriptions-channel-websocket/dist/inde
x.native.mjs
1.27KB -
rpc-subscriptions-channel-websocket/dist/inde
x.browser.mjs
1.26KB -
program-client-core/dist/index.browser.mjs
1.21KB -
program-client-core/dist/index.native.mjs
1.21KB -
program-client-core/dist/index.node.mjs
1.21KB -
options/dist/index.browser.mjs
1.18KB -
options/dist/index.native.mjs
1.18KB -
options/dist/index.node.mjs
1.17KB -
accounts/dist/index.browser.mjs
1.17KB -
accounts/dist/index.native.mjs
1.17KB -
accounts/dist/index.node.mjs
1.16KB -
compat/dist/index.browser.mjs
969B -
compat/dist/index.native.mjs
968B -
compat/dist/index.node.mjs
966B -
rpc-subscriptions-api/dist/index.native.mjs
871B -
rpc-subscriptions-api/dist/index.browser.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
870B -
promises/dist/index.native.mjs
841B -
promises/dist/index.node.mjs
840B -
promises/dist/index.browser.mjs
839B -
plugin-core/dist/index.browser.mjs
799B -
plugin-core/dist/index.native.mjs
798B -
plugin-core/dist/index.node.mjs
796B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
771B -
instructions/dist/index.native.mjs
770B -
instructions/dist/index.node.mjs
768B -
fast-stable-stringify/dist/index.browser.mjs
726B -
fast-stable-stringify/dist/index.native.mjs
725B -
assertions/dist/index.native.mjs
724B -
fast-stable-stringify/dist/index.node.mjs
724B -
assertions/dist/index.node.mjs
723B -
programs/dist/index.browser.mjs
329B -
programs/dist/index.native.mjs
327B -
programs/dist/index.node.mjs
325B -
fs-impl/dist/index.browser.mjs
245B -
event-target-impl/dist/index.node.mjs
230B -
functional/dist/index.browser.mjs
154B -
functional/dist/index.native.mjs
152B -
text-encoding-impl/dist/index.native.mjs
152B -
functional/dist/index.node.mjs
151B -
codecs/dist/index.browser.mjs
145B -
codecs/dist/index.native.mjs
144B -
codecs/dist/index.node.mjs
142B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
fs-impl/dist/index.node.mjs
120B -
text-encoding-impl/dist/index.node.mjs
119B -
ws-impl/dist/index.browser.mjs
113B -
crypto-impl/dist/index.node.mjs
111B -
crypto-impl/dist/index.browser.mjs
109B -
rpc-parsed-types/dist/index.browser.mjs
66B -
rpc-parsed-types/dist/index.native.mjs
65B -
rpc-parsed-types/dist/index.node.mjs
63B -

Total files change -110B -0.02%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@mcintyre94 mcintyre94 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is great, thankyou! My only concern is the duplication, given that we might need to update these in future. My instinct is to move it to the shared rpc-spec-types, which is already a dependency of both rpc-spec and rpc-subscriptions-spec. WDYT?

Symbol.asyncIterator,
Symbol.iterator,
Symbol.toPrimitive,
Symbol.toStringTag,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it'd be worth adding dispose and asyncDispose, just because accidentally doing using rpc = ... is slightly-plausible.

@ChargingFoxSec

Copy link
Copy Markdown
Contributor Author

Yeah, that makes sense. I’ll move the shared non-RPC property list into rpc-spec-types and add dispose and asyncDispose as well.

@mcintyre94 mcintyre94 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, reducing the duplication is definitely nicer! :)

Comment on lines 117 to 126
it('does not expose JS protocol hooks as RPC methods', () => {
expect.assertions(7);
expect.hasAssertions();
const api = createJsonRpcApi<DummyApi>();

expect(api).not.toHaveProperty('then');
expect(api).not.toHaveProperty('toJSON');
expect(getUntypedProperty(api, Symbol.asyncIterator)).toBeUndefined();
expect(getUntypedProperty(api, Symbol.for('nodejs.util.inspect.custom'))).toBeUndefined();
expect(getUntypedProperty(api, Symbol.iterator)).toBeUndefined();
expect(getUntypedProperty(api, Symbol.toPrimitive)).toBeUndefined();
expect(getUntypedProperty(api, Symbol.toStringTag)).toBeUndefined();
JAVASCRIPT_PROTOCOL_SYMBOLS.forEach(symbol => {
expect(getUntypedProperty(api, symbol)).toBeUndefined();
});
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: a cleaner way to write these tests would be using it.each, for example used here:

it.each([AccountRole.READONLY, AccountRole.WRITABLE])('returns `false` for AccountRole.$role', role => {

This would give us much better feedback if any of the tests fails

Comment on lines +3 to +6
const SymbolWithDispose = Symbol as typeof Symbol & {
asyncDispose?: symbol;
dispose?: symbol;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We shouldn't need this - Symbol.dispose and Symbol.asyncDispose should be available in the verison of Typescript we're using, and are available for me on main.

Comment on lines +106 to +109
expect.hasAssertions();
JAVASCRIPT_PROTOCOL_SYMBOLS.forEach(symbol => {
expect(getUntypedProperty(rpc, symbol)).toBeUndefined();
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same comment re it.each here

@ChargingFoxSec

Copy link
Copy Markdown
Contributor Author

Thanks, updated this.

I removed the custom Symbol typing and now use Symbol.dispose / Symbol.asyncDispose directly. Also changed the symbol checks to it.each so failures point at the specific symbol, and mirrored that in the subscription proxy tests.

Related node/browser tests, typecheck, lint, and Prettier checks are passing locally.

@mcintyre94 mcintyre94 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One last nit and this is ready to go, thankyou!

Comment on lines +32 to +41
export function getNonRpcPropertyValue(propertyName: string | symbol, receiver: unknown): unknown {
if (JAVASCRIPT_PROTOCOL_PROPERTY_NAMES.has(propertyName)) {
return undefined;
}
return Reflect.get(Object.prototype, propertyName, receiver);
}

export function isNonRpcPropertyName(propertyName: string | symbol): boolean {
return JAVASCRIPT_PROTOCOL_PROPERTY_NAMES.has(propertyName) || OBJECT_PROTOTYPE_PROPERTY_NAMES.has(propertyName);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: these are both exported functions. Can you add a docblock for each (just a one liner and reference to the issue is fine), and also @internal? I don't think these should be part of our API surface, but exporting them from here is correct. Sorry, should have flagged that when I suggested moving them here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Check if we should be intercepting properties in Proxy objects

2 participants