From d91a9afee889bded0d5dd201fc79adcd97c9314b Mon Sep 17 00:00:00 2001 From: steveluscher Date: Wed, 26 Jun 2024 21:42:39 +0000 Subject: [PATCH] fix: `onProgramAccountChange()` and `onAccountChange()` now accept an encoding --- packages/library-legacy/src/connection.ts | 76 ++++++++++- .../library-legacy/test/connection.test.ts | 125 +++++++++++++++++- 2 files changed, 195 insertions(+), 6 deletions(-) diff --git a/packages/library-legacy/src/connection.ts b/packages/library-legacy/src/connection.ts index fb89d00644c9..3f4c701ea8bf 100644 --- a/packages/library-legacy/src/connection.ts +++ b/packages/library-legacy/src/connection.ts @@ -2787,6 +2787,39 @@ export type GetNonceAndContextConfig = { minContextSlot?: number; }; +export type AccountSubscriptionConfig = Readonly<{ + /** Optional commitment level */ + commitment?: Commitment; + /** + * Encoding format for Account data + * - `base58` is slow. + * - `jsonParsed` encoding attempts to use program-specific state parsers to return more + * human-readable and explicit account state data + * - If `jsonParsed` is requested but a parser cannot be found, the field falls back to `base64` + * encoding, detectable when the `data` field is type `string`. + */ + encoding?: 'base58' | 'base64' | 'base64+zstd' | 'jsonParsed'; +}>; + +export type ProgramAccountSubscriptionConfig = Readonly<{ + /** Optional commitment level */ + commitment?: Commitment; + /** + * Encoding format for Account data + * - `base58` is slow. + * - `jsonParsed` encoding attempts to use program-specific state parsers to return more + * human-readable and explicit account state data + * - If `jsonParsed` is requested but a parser cannot be found, the field falls back to `base64` + * encoding, detectable when the `data` field is type `string`. + */ + encoding?: 'base58' | 'base64' | 'base64+zstd' | 'jsonParsed'; + /** + * Filter results using various filter objects + * The resultant account must meet ALL filter criteria to be included in the returned results + */ + filters?: GetProgramAccountsFilter[]; +}>; + /** * Information describing an account */ @@ -6334,18 +6367,34 @@ export class Connection { * * @param publicKey Public key of the account to monitor * @param callback Function to invoke whenever the account is changed - * @param commitment Specify the commitment level account changes must reach before notification + * @param config * @return subscription id */ + onAccountChange( + publicKey: PublicKey, + callback: AccountChangeCallback, + config?: AccountSubscriptionConfig, + ): ClientSubscriptionId; + /** @deprecated Instead, pass in an {@link AccountSubscriptionConfig} */ + // eslint-disable-next-line no-dupe-class-members onAccountChange( publicKey: PublicKey, callback: AccountChangeCallback, commitment?: Commitment, + ): ClientSubscriptionId; + // eslint-disable-next-line no-dupe-class-members + onAccountChange( + publicKey: PublicKey, + callback: AccountChangeCallback, + commitmentOrConfig?: Commitment | AccountSubscriptionConfig, ): ClientSubscriptionId { + const {commitment, config} = + extractCommitmentFromConfig(commitmentOrConfig); const args = this._buildArgs( [publicKey.toBase58()], commitment || this._commitment || 'finalized', // Apply connection/server default. 'base64', + config, ); return this._makeSubscription( { @@ -6394,21 +6443,40 @@ export class Connection { * * @param programId Public key of the program to monitor * @param callback Function to invoke whenever the account is changed - * @param commitment Specify the commitment level account changes must reach before notification - * @param filters The program account filters to pass into the RPC method + * @param config * @return subscription id */ + onProgramAccountChange( + programId: PublicKey, + callback: ProgramAccountChangeCallback, + config?: ProgramAccountSubscriptionConfig, + ): ClientSubscriptionId; + /** @deprecated Instead, pass in a {@link ProgramAccountSubscriptionConfig} */ + // eslint-disable-next-line no-dupe-class-members onProgramAccountChange( programId: PublicKey, callback: ProgramAccountChangeCallback, commitment?: Commitment, filters?: GetProgramAccountsFilter[], + ): ClientSubscriptionId; + // eslint-disable-next-line no-dupe-class-members + onProgramAccountChange( + programId: PublicKey, + callback: ProgramAccountChangeCallback, + commitmentOrConfig?: Commitment | ProgramAccountSubscriptionConfig, + maybeFilters?: GetProgramAccountsFilter[], ): ClientSubscriptionId { + const {commitment, config} = + extractCommitmentFromConfig(commitmentOrConfig); const args = this._buildArgs( [programId.toBase58()], commitment || this._commitment || 'finalized', // Apply connection/server default. 'base64' /* encoding */, - filters ? {filters: filters} : undefined /* extra */, + config + ? config + : maybeFilters + ? {filters: maybeFilters} + : undefined /* extra */, ); return this._makeSubscription( { diff --git a/packages/library-legacy/test/connection.test.ts b/packages/library-legacy/test/connection.test.ts index 2cb63ee499d3..84b90efadf1d 100644 --- a/packages/library-legacy/test/connection.test.ts +++ b/packages/library-legacy/test/connection.test.ts @@ -4,7 +4,7 @@ import {expect, use} from 'chai'; import chaiAsPromised from 'chai-as-promised'; import {Agent as HttpAgent} from 'http'; import {Agent as HttpsAgent} from 'https'; -import {match, mock, spy, useFakeTimers, SinonFakeTimers} from 'sinon'; +import {match, mock, spy, stub, useFakeTimers, SinonFakeTimers} from 'sinon'; import sinonChai from 'sinon-chai'; import {fail} from 'assert'; @@ -5914,7 +5914,7 @@ describe('Connection', function () { subscriptionId = connection.onAccountChange( owner.publicKey, resolve, - 'confirmed', + {commitment: 'confirmed'}, ); }, ); @@ -6394,4 +6394,125 @@ describe('Connection', function () { }); }).timeout(5 * 1000); } + + it('passes the commitment/encoding to the RPC when calling `onAccountChange`', () => { + const connection = new Connection(url); + const rpcRequestMethod = stub( + connection, + // @ts-expect-error This method is private, but none the less this spy will work. + '_makeSubscription', + ); + const mockCallback = () => {}; + connection.onAccountChange(PublicKey.default, mockCallback, { + commitment: 'processed', + encoding: 'base64+zstd', + }); + expect(rpcRequestMethod).to.have.been.calledWithExactly( + { + callback: mockCallback, + method: 'accountSubscribe', + unsubscribeMethod: 'accountUnsubscribe', + }, + [ + match.any, + match + .has('commitment', 'processed') + .and(match.has('encoding', 'base64+zstd')), + ], + ); + }); + it('passes the commitment to the RPC when the deprecated signature of `onAccountChange` is used', () => { + const connection = new Connection(url); + const rpcRequestMethod = stub( + connection, + // @ts-expect-error This method is private, but none the less this spy will work. + '_makeSubscription', + ); + const mockCallback = () => {}; + connection.onAccountChange(PublicKey.default, mockCallback, 'processed'); + expect(rpcRequestMethod).to.have.been.calledWithExactly( + { + callback: mockCallback, + method: 'accountSubscribe', + unsubscribeMethod: 'accountUnsubscribe', + }, + [match.any, match.has('commitment', 'processed')], + ); + }); + it('passes the commitment to the RPC when the deprecated signature of `onProgramAccountChange` is used', () => { + const connection = new Connection(url); + const rpcRequestMethod = stub( + connection, + // @ts-expect-error This method is private, but none the less this spy will work. + '_makeSubscription', + ); + const mockCallback = () => {}; + connection.onProgramAccountChange( + PublicKey.default, + mockCallback, + 'processed' /* commitment */, + ); + expect(rpcRequestMethod).to.have.been.calledWithExactly( + { + callback: mockCallback, + method: 'programSubscribe', + unsubscribeMethod: 'programUnsubscribe', + }, + [match.any, match.has('commitment', 'processed')], + ); + }); + it('passes the filters to the RPC when the deprecated signature of `onProgramAccountChange` is used', () => { + const connection = new Connection(url); + const rpcRequestMethod = stub( + connection, + // @ts-expect-error This method is private, but none the less this spy will work. + '_makeSubscription', + ); + const mockCallback = () => {}; + connection.onProgramAccountChange( + PublicKey.default, + mockCallback, + /* commitment */ undefined, + /* filters */ [{dataSize: 123}], + ); + expect(rpcRequestMethod).to.have.been.calledWithExactly( + { + callback: mockCallback, + method: 'programSubscribe', + unsubscribeMethod: 'programUnsubscribe', + }, + [match.any, match.has('filters', [{dataSize: 123}])], + ); + }); + it('passes the commitment/encoding/filters to the RPC when calling `onProgramAccountChange`', () => { + const connection = new Connection(url); + const rpcRequestMethod = stub( + connection, + // @ts-expect-error This method is private, but none the less this spy will work. + '_makeSubscription', + ); + const mockCallback = () => {}; + connection.onProgramAccountChange(PublicKey.default, mockCallback, { + commitment: 'processed', + encoding: 'base64+zstd', + filters: [{dataSize: 123}], + }); + expect(rpcRequestMethod).to.have.been.calledWithExactly( + { + callback: mockCallback, + method: 'programSubscribe', + unsubscribeMethod: 'programUnsubscribe', + }, + [ + match.any, + match + .has('commitment', 'processed') + .and( + match + .has('encoding', 'base64+zstd') + .and(match.has('filters', [{dataSize: 123}])), + ), + ], + ); + }); });