Skip to content

Commit

Permalink
feat(experimental): add accountNotifications websocket method (#1650)
Browse files Browse the repository at this point in the history
  • Loading branch information
buffalojoec authored Oct 6, 2023
1 parent e0b865d commit d9dcd1c
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 55 deletions.
93 changes: 39 additions & 54 deletions packages/rpc-core/src/response-patcher-allowed-numeric-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,42 @@ import { KEYPATH_WILDCARD } from './response-patcher-types';
import { createSolanaRpcApi } from './rpc-methods';
import { SolanaRpcSubscriptions, SolanaRpcSubscriptionsUnstable } from './rpc-subscriptions';

// Numeric values nested in `jsonParsed` accounts
const jsonParsedTokenAccountsConfigs = [
// parsed Token/Token22 token account
['data', 'parsed', 'info', 'tokenAmount', 'decimals'],
['data', 'parsed', 'info', 'tokenAmount', 'uiAmount'],
['data', 'parsed', 'info', 'rentExemptReserve', 'decimals'],
['data', 'parsed', 'info', 'rentExemptReserve', 'uiAmount'],
['data', 'parsed', 'info', 'delegatedAmount', 'decimals'],
['data', 'parsed', 'info', 'delegatedAmount', 'uiAmount'],
['data', 'parsed', 'info', 'extensions', KEYPATH_WILDCARD, 'state', 'olderTransferFee', 'transferFeeBasisPoints'],
['data', 'parsed', 'info', 'extensions', KEYPATH_WILDCARD, 'state', 'newerTransferFee', 'transferFeeBasisPoints'],
['data', 'parsed', 'info', 'extensions', KEYPATH_WILDCARD, 'state', 'preUpdateAverageRate'],
['data', 'parsed', 'info', 'extensions', KEYPATH_WILDCARD, 'state', 'currentRate'],
];
const jsonParsedAccountsConfigs = [
...jsonParsedTokenAccountsConfigs,
// parsed AddressTableLookup account
['data', 'parsed', 'info', 'lastExtendedSlotStartIndex'],
// parsed Config account
['data', 'parsed', 'info', 'slashPenalty'],
['data', 'parsed', 'info', 'warmupCooldownRate'],
// parsed Token/Token22 mint account
['data', 'parsed', 'info', 'decimals'],
// parsed Token/Token22 multisig account
['data', 'parsed', 'info', 'numRequiredSigners'],
['data', 'parsed', 'info', 'numValidSigners'],
// parsed Stake account
['data', 'parsed', 'info', 'stake', 'delegation', 'warmupCooldownRate'],
// parsed Sysvar rent account
['data', 'parsed', 'info', 'exemptionThreshold'],
['data', 'parsed', 'info', 'burnPercent'],
// parsed Vote account
['data', 'parsed', 'info', 'commission'],
['data', 'parsed', 'info', 'votes', KEYPATH_WILDCARD, 'confirmationCount'],
];

type AllowedNumericKeypaths<TApi> = Partial<Record<keyof TApi, readonly KeyPath[]>>;

let memoizedNotificationKeypaths: AllowedNumericKeypaths<
Expand All @@ -20,7 +56,9 @@ export function getAllowedNumericKeypathsForNotification(): AllowedNumericKeypat
IRpcSubscriptionsApi<SolanaRpcSubscriptions & SolanaRpcSubscriptionsUnstable>
> {
if (!memoizedNotificationKeypaths) {
memoizedNotificationKeypaths = {};
memoizedNotificationKeypaths = {
accountNotifications: jsonParsedAccountsConfigs.map(c => ['value', ...c]),
};
}
return memoizedNotificationKeypaths;
}
Expand All @@ -31,59 +69,6 @@ export function getAllowedNumericKeypathsForNotification(): AllowedNumericKeypat
*/
export function getAllowedNumericKeypathsForResponse(): AllowedNumericKeypaths<ReturnType<typeof createSolanaRpcApi>> {
if (!memoizedResponseKeypaths) {
// Numeric values nested in `jsonParsed` accounts
const jsonParsedTokenAccountsConfigs = [
// parsed Token/Token22 token account
['data', 'parsed', 'info', 'tokenAmount', 'decimals'],
['data', 'parsed', 'info', 'tokenAmount', 'uiAmount'],
['data', 'parsed', 'info', 'rentExemptReserve', 'decimals'],
['data', 'parsed', 'info', 'rentExemptReserve', 'uiAmount'],
['data', 'parsed', 'info', 'delegatedAmount', 'decimals'],
['data', 'parsed', 'info', 'delegatedAmount', 'uiAmount'],
[
'data',
'parsed',
'info',
'extensions',
KEYPATH_WILDCARD,
'state',
'olderTransferFee',
'transferFeeBasisPoints',
],
[
'data',
'parsed',
'info',
'extensions',
KEYPATH_WILDCARD,
'state',
'newerTransferFee',
'transferFeeBasisPoints',
],
['data', 'parsed', 'info', 'extensions', KEYPATH_WILDCARD, 'state', 'preUpdateAverageRate'],
['data', 'parsed', 'info', 'extensions', KEYPATH_WILDCARD, 'state', 'currentRate'],
];
const jsonParsedAccountsConfigs = [
...jsonParsedTokenAccountsConfigs,
// parsed AddressTableLookup account
['data', 'parsed', 'info', 'lastExtendedSlotStartIndex'],
// parsed Config account
['data', 'parsed', 'info', 'slashPenalty'],
['data', 'parsed', 'info', 'warmupCooldownRate'],
// parsed Token/Token22 mint account
['data', 'parsed', 'info', 'decimals'],
// parsed Token/Token22 multisig account
['data', 'parsed', 'info', 'numRequiredSigners'],
['data', 'parsed', 'info', 'numValidSigners'],
// parsed Stake account
['data', 'parsed', 'info', 'stake', 'delegation', 'warmupCooldownRate'],
// parsed Sysvar rent account
['data', 'parsed', 'info', 'exemptionThreshold'],
['data', 'parsed', 'info', 'burnPercent'],
// parsed Vote account
['data', 'parsed', 'info', 'commission'],
['data', 'parsed', 'info', 'votes', KEYPATH_WILDCARD, 'confirmationCount'],
];
memoizedResponseKeypaths = {
getAccountInfo: jsonParsedAccountsConfigs.map(c => ['value', ...c]),
getBlock: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Commitment } from '../../rpc-methods';

describe('accountNotifications', () => {
([undefined, 'confirmed', 'finalized', 'processed'] as (Commitment | undefined)[]).forEach(commitment => {
describe(`when called with \`${commitment}\` commitment`, () => {
it.todo('produces account notifications');
});
});

describe('when called with base58 encoding', () => {
it.todo('produces account notifications with annotated base58 encoding');
});

describe('when called with base64 encoding', () => {
it.todo('produces account notifications with annotated base64 encoding');
});

describe('when called with base64+zstd encoding', () => {
it.todo('produces account notifications with annotated base64+zstd encoding');
});

describe('when called with jsonParsed encoding', () => {
describe('for an account without parse-able JSON data', () => {
it.todo('produces account notifications as base64');
});

describe('for an account with parse-able JSON data', () => {
it.todo('produces account notifications with parsed JSON data for AddressLookupTable account');

it.todo('produces account notifications with parsed JSON data for BpfLoaderUpgradeable account');

it.todo('produces account notifications with parsed JSON data for Config validator account');

it.todo('produces account notifications with parsed JSON data for Config stake account');

it.todo('produces account notifications with parsed JSON data for Nonce account');

it.todo('produces account notifications with parsed JSON data for SPL Token mint account');

it.todo('produces account notifications with parsed JSON data for SPL Token token account');

it.todo('produces account notifications with parsed JSON data for SPL token multisig account');

it.todo('produces account notifications with parsed JSON data for SPL Token 22 mint account');

it.todo('produces account notifications with parsed JSON data for Stake account');

it.todo('produces account notifications with parsed JSON data for Sysvar rent account');

it.todo('produces account notifications with parsed JSON data for Vote account');
});
});

describe('when called with no encoding', () => {
it.todo('produces account notifications with base58 data without an annotation');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */

import { Base58EncodedAddress } from '@solana/addresses';
import { PendingRpcSubscription, RpcSubscriptions } from '@solana/rpc-transport/dist/types/json-rpc-types';

import { LamportsUnsafeBeyond2Pow53Minus1 } from '../../lamports';
import {
Base58EncodedBytes,
Base58EncodedDataResponse,
Base64EncodedDataResponse,
Base64EncodedZStdCompressedDataResponse,
RpcResponse,
U64UnsafeBeyond2Pow53Minus1,
} from '../../rpc-methods/common';
import { AccountNotificationsApi } from '../account-notifications';

async () => {
const rpcSubscriptions = null as unknown as RpcSubscriptions<AccountNotificationsApi>;
// See scripts/fixtures/GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G.json
// data is 'test data'
// Note: In type tests, it doesn't matter if the account is actually JSON-parseable
const pubkey =
'GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G' as Base58EncodedAddress<'GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G'>;

type TNotificationBase = Readonly<{
executable: boolean;
lamports: LamportsUnsafeBeyond2Pow53Minus1;
owner: Base58EncodedAddress;
rentEpoch: U64UnsafeBeyond2Pow53Minus1;
}>;

// No optional configs
rpcSubscriptions.accountNotifications(pubkey) satisfies PendingRpcSubscription<
RpcResponse<
TNotificationBase & {
data: Base58EncodedBytes;
}
>
>;
rpcSubscriptions
.accountNotifications(pubkey)
.subscribe({ abortSignal: new AbortController().signal }) satisfies Promise<
AsyncIterable<
RpcResponse<
TNotificationBase & {
data: Base58EncodedBytes;
}
>
>
>;
// With optional configs
rpcSubscriptions.accountNotifications(pubkey, { commitment: 'confirmed' }) satisfies PendingRpcSubscription<
RpcResponse<
TNotificationBase & {
data: Base58EncodedBytes;
}
>
>;
rpcSubscriptions
.accountNotifications(pubkey, { commitment: 'confirmed' })
.subscribe({ abortSignal: new AbortController().signal }) satisfies Promise<
AsyncIterable<
RpcResponse<
TNotificationBase & {
data: Base58EncodedBytes;
}
>
>
>;
// Base58 encoded data
rpcSubscriptions.accountNotifications(pubkey, { encoding: 'base58' }) satisfies PendingRpcSubscription<
RpcResponse<
TNotificationBase & {
data: Base58EncodedDataResponse;
}
>
>;
rpcSubscriptions
.accountNotifications(pubkey, { encoding: 'base58' })
.subscribe({ abortSignal: new AbortController().signal }) satisfies Promise<
AsyncIterable<
RpcResponse<
TNotificationBase & {
data: Base58EncodedDataResponse;
}
>
>
>;
// Base64 encoded data
rpcSubscriptions.accountNotifications(pubkey, { encoding: 'base64' }) satisfies PendingRpcSubscription<
RpcResponse<
TNotificationBase & {
data: Base64EncodedDataResponse;
}
>
>;
rpcSubscriptions
.accountNotifications(pubkey, { encoding: 'base64' })
.subscribe({ abortSignal: new AbortController().signal }) satisfies Promise<
AsyncIterable<
RpcResponse<
TNotificationBase & {
data: Base64EncodedDataResponse;
}
>
>
>;
// Base64 + ZSTD encoded data
rpcSubscriptions.accountNotifications(pubkey, { encoding: 'base64+zstd' }) satisfies PendingRpcSubscription<
RpcResponse<
TNotificationBase & {
data: Base64EncodedZStdCompressedDataResponse;
}
>
>;
rpcSubscriptions
.accountNotifications(pubkey, { encoding: 'base64+zstd' })
.subscribe({ abortSignal: new AbortController().signal }) satisfies Promise<
AsyncIterable<
RpcResponse<
TNotificationBase & {
data: Base64EncodedZStdCompressedDataResponse;
}
>
>
>;
// JSON parsed data
rpcSubscriptions.accountNotifications(pubkey, { encoding: 'jsonParsed' }) satisfies PendingRpcSubscription<
RpcResponse<
TNotificationBase & {
data:
| Readonly<{
program: string;
parsed: unknown;
space: U64UnsafeBeyond2Pow53Minus1;
}>
| Base64EncodedDataResponse;
}
>
>;
rpcSubscriptions
.accountNotifications(pubkey, { encoding: 'jsonParsed' })
.subscribe({ abortSignal: new AbortController().signal }) satisfies Promise<
AsyncIterable<
RpcResponse<
TNotificationBase & {
data:
| Readonly<{
program: string;
parsed: unknown;
space: U64UnsafeBeyond2Pow53Minus1;
}>
| Base64EncodedDataResponse;
}
>
>
>;
};
58 changes: 58 additions & 0 deletions packages/rpc-core/src/rpc-subscriptions/account-notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Base58EncodedAddress } from '@solana/addresses';

import {
AccountInfoBase,
AccountInfoWithBase58Bytes,
AccountInfoWithBase58EncodedData,
AccountInfoWithBase64EncodedData,
AccountInfoWithBase64EncodedZStdCompressedData,
AccountInfoWithJsonData,
Commitment,
RpcResponse,
} from '../rpc-methods/common';

type AccountNotificationsApiCommonConfig = Readonly<{
commitment?: Commitment;
}>;

export interface AccountNotificationsApi {
/**
* Subscribe to an account to receive notifications when the lamports or data for
* a given account public key changes.
*
* The notification format is the same as seen in the `getAccountInfo` RPC HTTP method.
* @see https://docs.solana.com/api/websocket#getAccountInfo
*/
accountNotifications(
address: Base58EncodedAddress,
config: AccountNotificationsApiCommonConfig &
Readonly<{
encoding: 'base64';
}>
): RpcResponse<AccountInfoBase & AccountInfoWithBase64EncodedData>;
accountNotifications(
address: Base58EncodedAddress,
config: AccountNotificationsApiCommonConfig &
Readonly<{
encoding: 'base64+zstd';
}>
): RpcResponse<AccountInfoBase & AccountInfoWithBase64EncodedZStdCompressedData>;
accountNotifications(
address: Base58EncodedAddress,
config: AccountNotificationsApiCommonConfig &
Readonly<{
encoding: 'jsonParsed';
}>
): RpcResponse<AccountInfoBase & AccountInfoWithJsonData>;
accountNotifications(
address: Base58EncodedAddress,
config: AccountNotificationsApiCommonConfig &
Readonly<{
encoding: 'base58';
}>
): RpcResponse<AccountInfoBase & AccountInfoWithBase58EncodedData>;
accountNotifications(
address: Base58EncodedAddress,
config?: AccountNotificationsApiCommonConfig
): RpcResponse<AccountInfoBase & AccountInfoWithBase58Bytes>;
}
Loading

0 comments on commit d9dcd1c

Please sign in to comment.