Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/radik878_fix-listaccounts-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- implement public_keys filtering in ListAccounts and add tests
36 changes: 30 additions & 6 deletions validator/rpc/handlers_accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,33 +75,57 @@ func (s *Server) ListAccounts(w http.ResponseWriter, r *http.Request) {
httputil.HandleError(w, errors.Errorf("Could not retrieve public keys: %v", err).Error(), http.StatusInternalServerError)
return
}
accs := make([]*Account, len(keys))
// Build optional filter set from query public_keys.
var filterSet map[string]struct{}
if len(pubkeys) > 0 {
filterSet = make(map[string]struct{}, len(pubkeys))
for _, pk := range pubkeys {
filterSet[string(pk)] = struct{}{}
}
}
// Build accounts list, optionally filtering by provided public_keys.
accs := make([]*Account, 0, len(keys))
for i := 0; i < len(keys); i++ {
accs[i] = &Account{
if filterSet != nil {
if _, ok := filterSet[string(keys[i][:])]; !ok {
continue
}
}
acc := &Account{
ValidatingPublicKey: hexutil.Encode(keys[i][:]),
AccountName: petnames.DeterministicName(keys[i][:], "-"),
}
if s.wallet.KeymanagerKind() == keymanager.Derived {
accs[i].DerivationPath = fmt.Sprintf(derived.ValidatingKeyDerivationPathTemplate, i)
acc.DerivationPath = fmt.Sprintf(derived.ValidatingKeyDerivationPathTemplate, i)
}
accs = append(accs, acc)
}
if r.URL.Query().Get("all") == "true" {
httputil.WriteJson(w, &ListAccountsResponse{
Accounts: accs,
TotalSize: int32(len(keys)),
TotalSize: int32(len(accs)),
NextPageToken: "",
})
return
}
// If no accounts after filtering, return an empty page.
if len(accs) == 0 {
httputil.WriteJson(w, &ListAccountsResponse{
Accounts: accs,
TotalSize: 0,
NextPageToken: "",
})
return
}
start, end, nextPageToken, err := pagination.StartAndEndPage(pageToken, int(ps), len(keys))
start, end, nextPageToken, err := pagination.StartAndEndPage(pageToken, int(ps), len(accs))
if err != nil {
httputil.HandleError(w, fmt.Errorf("Could not paginate results: %w",
err).Error(), http.StatusInternalServerError)
return
}
httputil.WriteJson(w, &ListAccountsResponse{
Accounts: accs[start:end],
TotalSize: int32(len(keys)),
TotalSize: int32(len(accs)),
NextPageToken: nextPageToken,
})
}
Expand Down
100 changes: 100 additions & 0 deletions validator/rpc/handlers_accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,103 @@ func TestServer_VoluntaryExit(t *testing.T) {
}

}

func TestServer_ListAccounts_FilterAndPagination(t *testing.T) {
ctx := t.Context()
localWalletDir := setupWalletDir(t)
defaultWalletPath = localWalletDir
// Create wallet with derived keymanager and recover N accounts
opts := []accounts.Option{
accounts.WithWalletDir(defaultWalletPath),
accounts.WithKeymanagerType(keymanager.Derived),
accounts.WithWalletPassword(strongPass),
accounts.WithSkipMnemonicConfirm(true),
}
acc, err := accounts.NewCLIManager(opts...)
require.NoError(t, err)
w, err := acc.WalletCreate(ctx)
require.NoError(t, err)
km, err := w.InitializeKeymanager(ctx, iface.InitKeymanagerConfig{ListenForChanges: false})
require.NoError(t, err)
vs, err := client.NewValidatorService(ctx, &client.Config{
Wallet: w,
Validator: &testutil.FakeValidator{
Km: km,
},
})
require.NoError(t, err)
s := &Server{
walletInitialized: true,
wallet: w,
validatorService: vs,
}
// Recover multiple accounts
numAccounts := 10
dr, ok := km.(*derived.Keymanager)
require.Equal(t, true, ok)
err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, derived.DefaultMnemonicLanguage, "", numAccounts)
require.NoError(t, err)

// Fetch all accounts to pick two pubkeys for filtering
req := httptest.NewRequest(http.MethodGet, api.WebUrlPrefix+"accounts?all=true", nil)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.ListAccounts(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
resp := &ListAccountsResponse{}
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), resp))
if len(resp.Accounts) < 2 {
t.Fatalf("expected at least 2 accounts, got %d", len(resp.Accounts))
}

target1 := resp.Accounts[1]
target2 := resp.Accounts[3]

// Page 1: page_size=1, filtered by two pubkeys
url1 := api.WebUrlPrefix + "accounts?page_size=1" +
"&public_keys=" + target1.ValidatingPublicKey +
"&public_keys=" + target2.ValidatingPublicKey
req = httptest.NewRequest(http.MethodGet, url1, nil)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.ListAccounts(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
page1 := &ListAccountsResponse{}
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), page1))
require.Equal(t, int32(2), page1.TotalSize)
require.Equal(t, 1, len(page1.Accounts))
assert.Equal(t, target1.ValidatingPublicKey, page1.Accounts[0].ValidatingPublicKey)
require.NotEmpty(t, page1.NextPageToken)

// Page 2: use next page token
url2 := api.WebUrlPrefix + "accounts?page_size=1&page_token=" + page1.NextPageToken +
"&public_keys=" + target1.ValidatingPublicKey +
"&public_keys=" + target2.ValidatingPublicKey
req = httptest.NewRequest(http.MethodGet, url2, nil)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.ListAccounts(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
page2 := &ListAccountsResponse{}
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), page2))
require.Equal(t, int32(2), page2.TotalSize)
require.Equal(t, 1, len(page2.Accounts))
assert.Equal(t, target2.ValidatingPublicKey, page2.Accounts[0].ValidatingPublicKey)

// all=true: both filtered accounts returned
urlAll := api.WebUrlPrefix + "accounts?all=true" +
"&public_keys=" + target1.ValidatingPublicKey +
"&public_keys=" + target2.ValidatingPublicKey
req = httptest.NewRequest(http.MethodGet, urlAll, nil)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.ListAccounts(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
allResp := &ListAccountsResponse{}
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), allResp))
require.Equal(t, int32(2), allResp.TotalSize)
require.Equal(t, 2, len(allResp.Accounts))
// Order should reflect the original order by index in key list
assert.Equal(t, target1.ValidatingPublicKey, allResp.Accounts[0].ValidatingPublicKey)
assert.Equal(t, target2.ValidatingPublicKey, allResp.Accounts[1].ValidatingPublicKey)
}