Skip to content

Commit 62a4602

Browse files
committed
feat(ws): support dataSlice in ProgramSubscribe
Add ProgramSubscribeOpts and ProgramSubscribeWithConfig to mirror the optional configuration object the programSubscribe RPC method accepts, including the missing dataSlice parameter. dataSlice asks the validator to return only the requested slice of each notified account's data field. For programSubscribe that's especially important: every account owned by the program emits notifications, so trimming the payload to the relevant header bytes (discriminators, mint, owner, amount, etc.) avoids shipping the full account data on every update. The existing ProgramSubscribeWithOpts(programID, commitment, encoding, filters) entry point delegates to the new opts form, so existing callers are not affected. Adds wire-shape tests covering: defaults, dataSlice JSON shape ({"offset": N, "length": N}), and dataSlice combined with filters.
1 parent 20713fb commit 62a4602

2 files changed

Lines changed: 139 additions & 10 deletions

File tree

rpc/ws/programSubscribe.go

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,27 @@ type ProgramResult struct {
2828
Value rpc.KeyedAccount `json:"value"`
2929
}
3030

31+
// ProgramSubscribeOpts mirrors the optional configuration object the
32+
// programSubscribe RPC method accepts. See
33+
// https://solana.com/docs/rpc/websocket/programsubscribe.
34+
type ProgramSubscribeOpts struct {
35+
// Commitment selects the bank state the validator should query.
36+
Commitment rpc.CommitmentType
37+
38+
// Encoding controls how the account data field is encoded. When
39+
// empty the request defaults to "base64".
40+
Encoding solana.EncodingType
41+
42+
// Filters narrows the stream of account-change notifications to
43+
// the subset that match every supplied filter (memcmp/dataSize).
44+
Filters []rpc.RPCFilter
45+
46+
// DataSlice asks the validator to return only the requested slice
47+
// of each notified account's data field. Only valid for binary
48+
// encodings (base58, base64, base64+zstd) per the RPC spec.
49+
DataSlice *rpc.DataSlice
50+
}
51+
3152
// ProgramSubscribe subscribes to a program to receive notifications
3253
// when the lamports or data for a given account owned by the program changes.
3354
func (cl *Client) ProgramSubscribe(
@@ -42,27 +63,47 @@ func (cl *Client) ProgramSubscribe(
4263
)
4364
}
4465

45-
// ProgramSubscribe subscribes to a program to receive notifications
46-
// when the lamports or data for a given account owned by the program changes.
66+
// ProgramSubscribeWithOpts subscribes to a program with explicit
67+
// commitment, encoding, and filters. Kept for backward compatibility;
68+
// new callers should prefer ProgramSubscribeWithConfig which exposes
69+
// the full optional configuration object (including DataSlice).
4770
func (cl *Client) ProgramSubscribeWithOpts(
4871
programID solana.PublicKey,
4972
commitment rpc.CommitmentType,
5073
encoding solana.EncodingType,
5174
filters []rpc.RPCFilter,
5275
) (*ProgramSubscription, error) {
76+
return cl.ProgramSubscribeWithConfig(programID, &ProgramSubscribeOpts{
77+
Commitment: commitment,
78+
Encoding: encoding,
79+
Filters: filters,
80+
})
81+
}
5382

83+
// ProgramSubscribeWithConfig subscribes to a program and forwards the
84+
// full ProgramSubscribeOpts configuration object to the validator,
85+
// including DataSlice.
86+
func (cl *Client) ProgramSubscribeWithConfig(
87+
programID solana.PublicKey,
88+
opts *ProgramSubscribeOpts,
89+
) (*ProgramSubscription, error) {
5490
params := []any{programID.String()}
5591
conf := map[string]any{
5692
"encoding": "base64",
5793
}
58-
if commitment != "" {
59-
conf["commitment"] = commitment
60-
}
61-
if encoding != "" {
62-
conf["encoding"] = encoding
63-
}
64-
if len(filters) > 0 {
65-
conf["filters"] = filters
94+
if opts != nil {
95+
if opts.Commitment != "" {
96+
conf["commitment"] = opts.Commitment
97+
}
98+
if opts.Encoding != "" {
99+
conf["encoding"] = opts.Encoding
100+
}
101+
if len(opts.Filters) > 0 {
102+
conf["filters"] = opts.Filters
103+
}
104+
if opts.DataSlice != nil {
105+
conf["dataSlice"] = opts.DataSlice
106+
}
66107
}
67108

68109
genSub, err := cl.subscribe(

rpc/ws/programSubscribe_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2026 github.com/gagliardetto
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ws
16+
17+
import (
18+
"testing"
19+
20+
stdjson "github.com/goccy/go-json"
21+
"github.com/stretchr/testify/require"
22+
23+
"github.com/gagliardetto/solana-go"
24+
"github.com/gagliardetto/solana-go/rpc"
25+
)
26+
27+
// buildProgramSubscribeConf re-runs the same config-object construction
28+
// path ProgramSubscribeWithConfig would send to the validator, so the
29+
// test can assert on the wire shape without a live websocket.
30+
func buildProgramSubscribeConf(opts *ProgramSubscribeOpts) map[string]any {
31+
conf := map[string]any{
32+
"encoding": "base64",
33+
}
34+
if opts != nil {
35+
if opts.Commitment != "" {
36+
conf["commitment"] = opts.Commitment
37+
}
38+
if opts.Encoding != "" {
39+
conf["encoding"] = opts.Encoding
40+
}
41+
if len(opts.Filters) > 0 {
42+
conf["filters"] = opts.Filters
43+
}
44+
if opts.DataSlice != nil {
45+
conf["dataSlice"] = opts.DataSlice
46+
}
47+
}
48+
return conf
49+
}
50+
51+
func TestProgramSubscribeConfDefaults(t *testing.T) {
52+
conf := buildProgramSubscribeConf(nil)
53+
require.Equal(t, "base64", conf["encoding"])
54+
require.NotContains(t, conf, "commitment")
55+
require.NotContains(t, conf, "filters")
56+
require.NotContains(t, conf, "dataSlice")
57+
}
58+
59+
func TestProgramSubscribeConfWithDataSlice(t *testing.T) {
60+
off, length := uint64(165), uint64(72)
61+
conf := buildProgramSubscribeConf(&ProgramSubscribeOpts{
62+
Commitment: rpc.CommitmentConfirmed,
63+
Encoding: solana.EncodingBase64,
64+
DataSlice: &rpc.DataSlice{Offset: &off, Length: &length},
65+
})
66+
require.Equal(t, rpc.CommitmentConfirmed, conf["commitment"])
67+
68+
// Round-trip the dataSlice through JSON to confirm the wire shape
69+
// matches the RPC spec ({"offset": N, "length": N}).
70+
encoded, err := stdjson.Marshal(conf["dataSlice"])
71+
require.NoError(t, err)
72+
require.JSONEq(t, `{"offset":165,"length":72}`, string(encoded))
73+
}
74+
75+
func TestProgramSubscribeConfWithDataSliceAndFilters(t *testing.T) {
76+
off, length := uint64(0), uint64(8)
77+
conf := buildProgramSubscribeConf(&ProgramSubscribeOpts{
78+
Filters: []rpc.RPCFilter{
79+
{DataSize: 165},
80+
},
81+
DataSlice: &rpc.DataSlice{Offset: &off, Length: &length},
82+
})
83+
require.Contains(t, conf, "filters")
84+
require.Contains(t, conf, "dataSlice")
85+
filters := conf["filters"].([]rpc.RPCFilter)
86+
require.Len(t, filters, 1)
87+
require.Equal(t, uint64(165), filters[0].DataSize)
88+
}

0 commit comments

Comments
 (0)