Skip to content

Commit 84919c9

Browse files
committed
feat(rpc): forward MinContextSlot in getProgramAccounts and getTokenAccounts
Three Solana RPC methods accept an optional `minContextSlot` per the spec but the Go bindings dropped it on the floor: - getProgramAccounts (via GetProgramAccountsOpts) - getTokenAccountsByOwner (via GetTokenAccountsOpts) - getTokenAccountsByDelegate (via GetTokenAccountsOpts) Without `minContextSlot`, callers on lagging RPC nodes can read state from a slot older than what they have already observed, which breaks read-your-own-writes and slot-pinned pagination flows. Follow-up to #245, which added the same field to GetMultipleAccountsWithOpts. Pattern is identical: a new `MinContextSlot *uint64` on the existing opts struct, forwarded into the params object only when non-nil. Purely additive. Tests pin the wire shape for each of the three methods using the existing mockJSONRPC harness.
1 parent 75c68a3 commit 84919c9

5 files changed

Lines changed: 153 additions & 0 deletions

File tree

rpc/getMinContextSlot_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package rpc
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/gagliardetto/solana-go"
8+
stdjson "github.com/goccy/go-json"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// TestClient_GetProgramAccountsWithOpts_MinContextSlot pins that the
14+
// minContextSlot opt is forwarded into the params object.
15+
func TestClient_GetProgramAccountsWithOpts_MinContextSlot(t *testing.T) {
16+
responseBody := `[{"account":{"data":["dGVzdA==","base64"],"executable":true,"lamports":2039280,"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","rentEpoch":206},"pubkey":"7xLk17EQQ5KLDLDe44wCmupJKJjTGd8hs3eSVVhCx932"}]`
17+
server, closer := mockJSONRPC(t, stdjson.RawMessage(wrapIntoRPC(responseBody)))
18+
defer closer()
19+
client := New(server.URL)
20+
21+
pubkeyString := "7xLk17EQQ5KLDLDe44wCmupJKJjTGd8hs3eSVVhCx932"
22+
pubKey := solana.MustPublicKeyFromBase58(pubkeyString)
23+
24+
minSlot := uint64(123456789)
25+
_, err := client.GetProgramAccountsWithOpts(
26+
context.Background(),
27+
pubKey,
28+
&GetProgramAccountsOpts{
29+
MinContextSlot: &minSlot,
30+
},
31+
)
32+
require.NoError(t, err)
33+
34+
reqBody := server.RequestBody(t)
35+
reqBody["id"] = any(nil)
36+
37+
assert.Equal(t,
38+
map[string]any{
39+
"id": any(nil),
40+
"jsonrpc": "2.0",
41+
"method": "getProgramAccounts",
42+
"params": []any{
43+
pubkeyString,
44+
map[string]any{
45+
"encoding": "base64",
46+
"minContextSlot": float64(minSlot),
47+
},
48+
},
49+
},
50+
reqBody,
51+
)
52+
}
53+
54+
// TestClient_GetTokenAccountsByOwner_MinContextSlot pins that the
55+
// minContextSlot opt is forwarded into the params object for the owner variant.
56+
func TestClient_GetTokenAccountsByOwner_MinContextSlot(t *testing.T) {
57+
responseBody := `{"context":{"slot":1},"value":[]}`
58+
server, closer := mockJSONRPC(t, stdjson.RawMessage(wrapIntoRPC(responseBody)))
59+
defer closer()
60+
client := New(server.URL)
61+
62+
ownerString := "7xLk17EQQ5KLDLDe44wCmupJKJjTGd8hs3eSVVhCx932"
63+
owner := solana.MustPublicKeyFromBase58(ownerString)
64+
programIDString := "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
65+
programID := solana.MustPublicKeyFromBase58(programIDString)
66+
67+
minSlot := uint64(987654321)
68+
_, err := client.GetTokenAccountsByOwner(
69+
context.Background(),
70+
owner,
71+
&GetTokenAccountsConfig{ProgramId: &programID},
72+
&GetTokenAccountsOpts{MinContextSlot: &minSlot},
73+
)
74+
require.NoError(t, err)
75+
76+
reqBody := server.RequestBody(t)
77+
reqBody["id"] = any(nil)
78+
79+
assert.Equal(t,
80+
map[string]any{
81+
"id": any(nil),
82+
"jsonrpc": "2.0",
83+
"method": "getTokenAccountsByOwner",
84+
"params": []any{
85+
ownerString,
86+
map[string]any{"programId": programIDString},
87+
map[string]any{
88+
"encoding": "base64",
89+
"minContextSlot": float64(minSlot),
90+
},
91+
},
92+
},
93+
reqBody,
94+
)
95+
}
96+
97+
// TestClient_GetTokenAccountsByDelegate_MinContextSlot pins the same for the
98+
// delegate variant.
99+
func TestClient_GetTokenAccountsByDelegate_MinContextSlot(t *testing.T) {
100+
responseBody := `{"context":{"slot":1},"value":[]}`
101+
server, closer := mockJSONRPC(t, stdjson.RawMessage(wrapIntoRPC(responseBody)))
102+
defer closer()
103+
client := New(server.URL)
104+
105+
delegateString := "7xLk17EQQ5KLDLDe44wCmupJKJjTGd8hs3eSVVhCx932"
106+
delegate := solana.MustPublicKeyFromBase58(delegateString)
107+
programIDString := "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
108+
programID := solana.MustPublicKeyFromBase58(programIDString)
109+
110+
minSlot := uint64(42)
111+
_, err := client.GetTokenAccountsByDelegate(
112+
context.Background(),
113+
delegate,
114+
&GetTokenAccountsConfig{ProgramId: &programID},
115+
&GetTokenAccountsOpts{MinContextSlot: &minSlot},
116+
)
117+
require.NoError(t, err)
118+
119+
reqBody := server.RequestBody(t)
120+
reqBody["id"] = any(nil)
121+
122+
assert.Equal(t,
123+
map[string]any{
124+
"id": any(nil),
125+
"jsonrpc": "2.0",
126+
"method": "getTokenAccountsByDelegate",
127+
"params": []any{
128+
delegateString,
129+
map[string]any{"programId": programIDString},
130+
map[string]any{
131+
"encoding": "base64",
132+
"minContextSlot": float64(minSlot),
133+
},
134+
},
135+
},
136+
reqBody,
137+
)
138+
}

rpc/getProgramAccounts.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ func buildGetProgramAccountsParams(publicKey solana.PublicKey, opts *GetProgramA
9090
if opts.SortResults != nil {
9191
obj["sortResults"] = *opts.SortResults
9292
}
93+
if opts.MinContextSlot != nil {
94+
obj["minContextSlot"] = *opts.MinContextSlot
95+
}
9396
}
9497
return []any{publicKey, obj}
9598
}

rpc/getTokenAccountsByDelegate.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ type GetTokenAccountsOpts struct {
3737
Encoding solana.EncodingType `json:"encoding,omitempty"`
3838

3939
DataSlice *DataSlice `json:"dataSlice,omitempty"`
40+
41+
// The minimum slot that the request can be evaluated at.
42+
MinContextSlot *uint64 `json:"minContextSlot,omitempty"`
4043
}
4144

4245
// GetTokenAccountsByDelegate returns all SPL Token accounts by approved Delegate.
@@ -87,6 +90,9 @@ func (cl *Client) GetTokenAccountsByDelegate(
8790
return nil, errors.New("cannot use dataSlice with EncodingJSONParsed")
8891
}
8992
}
93+
if opts.MinContextSlot != nil {
94+
optsObj["minContextSlot"] = *opts.MinContextSlot
95+
}
9096
if len(optsObj) > 0 {
9197
params = append(params, optsObj)
9298
}

rpc/getTokenAccountsByOwner.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ func (cl *Client) GetTokenAccountsByOwner(
6969
return nil, errors.New("cannot use dataSlice with EncodingJSONParsed")
7070
}
7171
}
72+
if opts.MinContextSlot != nil {
73+
optsObj["minContextSlot"] = *opts.MinContextSlot
74+
}
7275
if len(optsObj) > 0 {
7376
params = append(params, optsObj)
7477
}

rpc/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,9 @@ type GetProgramAccountsOpts struct {
466466

467467
// Sort the results (useful for deterministic pagination).
468468
SortResults *bool `json:"sortResults,omitempty"`
469+
470+
// The minimum slot that the request can be evaluated at.
471+
MinContextSlot *uint64 `json:"minContextSlot,omitempty"`
469472
}
470473

471474
type GetProgramAccountsResult []*KeyedAccount

0 commit comments

Comments
 (0)