Skip to content

Commit

Permalink
api: v2alpha1 account service (#5982)
Browse files Browse the repository at this point in the history
```
service AccountService {
  rpc List(AccountRequest) returns (AccountList);
}
```

spacemeshos/api#337
  • Loading branch information
kacpersaw committed May 29, 2024
1 parent 98a49a4 commit 0db7e0a
Show file tree
Hide file tree
Showing 8 changed files with 439 additions and 0 deletions.
2 changes: 2 additions & 0 deletions api/grpcserver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const (
LayerStreamV2Alpha1 Service = "layer_stream_v2alpha1"
TransactionV2Alpha1 Service = "transaction_v2alpha1"
TransactionStreamV2Alpha1 Service = "transaction_stream_v2alpha1"
AccountV2Alpha1 Service = "account_v2alpha1"
)

// DefaultConfig defines the default configuration options for api.
Expand All @@ -56,6 +57,7 @@ func DefaultConfig() Config {
PublicServices: []Service{
GlobalState, Mesh, Transaction, Node, Activation, ActivationV2Alpha1,
RewardV2Alpha1, NetworkV2Alpha1, NodeV2Alpha1, LayerV2Alpha1, TransactionV2Alpha1,
AccountV2Alpha1,
},
PublicListener: "0.0.0.0:9092",
PrivateServices: []Service{
Expand Down
136 changes: 136 additions & 0 deletions api/grpcserver/v2alpha1/account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package v2alpha1

import (
"context"

"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
spacemeshv2alpha1 "github.com/spacemeshos/api/release/go/spacemesh/v2alpha1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/spacemeshos/go-spacemesh/common/types"
"github.com/spacemeshos/go-spacemesh/sql"
"github.com/spacemeshos/go-spacemesh/sql/accounts"
"github.com/spacemeshos/go-spacemesh/sql/builder"
)

const (
Account = "account_v2alpha1"
)

type accountConState interface {
GetProjection(types.Address) (uint64, uint64)
}

func NewAccountService(db sql.Executor, conState accountConState) *AccountService {
return &AccountService{
db: db,
conState: conState,
}
}

type AccountService struct {
db sql.Executor
conState accountConState
}

func (s *AccountService) RegisterService(server *grpc.Server) {
spacemeshv2alpha1.RegisterAccountServiceServer(server, s)
}

func (s *AccountService) RegisterHandlerService(mux *runtime.ServeMux) error {
return spacemeshv2alpha1.RegisterAccountServiceHandlerServer(context.Background(), mux, s)
}

// String returns the service name.
func (s *AccountService) String() string {
return "AccountService"
}

func (s *AccountService) List(
ctx context.Context,
request *spacemeshv2alpha1.AccountRequest,
) (*spacemeshv2alpha1.AccountList, error) {
ops, err := toAccountOperations(request)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

switch {
case request.Limit > 100:
return nil, status.Error(codes.InvalidArgument, "limit is capped at 100")
case request.Limit == 0:
return nil, status.Error(codes.InvalidArgument, "limit must be set to <= 100")
}

rst := make([]*spacemeshv2alpha1.Account, 0, request.Limit)
if err := accounts.IterateAccountsOps(s.db, ops, func(account *types.Account) bool {
counterProjected, balanceProjected := s.conState.GetProjection(account.Address)
rst = append(rst, &spacemeshv2alpha1.Account{
Address: account.Address.String(),
Current: &spacemeshv2alpha1.AccountState{
Counter: account.NextNonce,
Balance: account.Balance,
Layer: account.Layer.Uint32(),
},
Projected: &spacemeshv2alpha1.AccountState{
Counter: counterProjected,
Balance: balanceProjected,
},
})
return true
}); err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &spacemeshv2alpha1.AccountList{Accounts: rst}, nil
}

func toAccountOperations(filter *spacemeshv2alpha1.AccountRequest) (builder.Operations, error) {
ops := builder.Operations{}
if filter == nil {
return ops, nil
}

if len(filter.Addresses) > 0 {
var addrs [][]byte
for _, address := range filter.Addresses {
addr, err := types.StringToAddress(address)
if err != nil {
return builder.Operations{}, err
}
addrs = append(addrs, addr.Bytes())
}

ops.Filter = append(ops.Filter, builder.Op{
Field: builder.Address,
Token: builder.In,
Value: addrs,
})
}

ops.Modifiers = append(ops.Modifiers, builder.Modifier{
Key: builder.GroupBy,
Value: "address",
})

ops.Modifiers = append(ops.Modifiers, builder.Modifier{
Key: builder.OrderBy,
Value: "layer_updated desc",
})

if filter.Limit != 0 {
ops.Modifiers = append(ops.Modifiers, builder.Modifier{
Key: builder.Limit,
Value: int64(filter.Limit),
})
}
if filter.Offset != 0 {
ops.Modifiers = append(ops.Modifiers, builder.Modifier{
Key: builder.Offset,
Value: int64(filter.Offset),
})
}

return ops, nil
}
79 changes: 79 additions & 0 deletions api/grpcserver/v2alpha1/account_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

137 changes: 137 additions & 0 deletions api/grpcserver/v2alpha1/account_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package v2alpha1

import (
"context"
"encoding/binary"
"fmt"
"testing"

spacemeshv2alpha1 "github.com/spacemeshos/api/release/go/spacemesh/v2alpha1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/spacemeshos/go-spacemesh/common/types"
"github.com/spacemeshos/go-spacemesh/sql"
"github.com/spacemeshos/go-spacemesh/sql/accounts"
)

type testAccount struct {
Address types.Address
BalanceCurrent uint64
BalanceProjected uint64
CounterCurrent uint64
CounterProjected uint64
}

func TestAccountService_List(t *testing.T) {
db := sql.InMemory()

ctrl, ctx := gomock.WithContext(context.Background(), t)
conState := NewMockaccountConState(ctrl)

accs := make([]testAccount, 100)
for i := 0; i < 100; i++ {
addr := types.Address{}
binary.BigEndian.PutUint64(addr[:], uint64(i))

accs[i] = testAccount{
Address: addr,
BalanceCurrent: uint64(1 + i),
BalanceProjected: uint64(100 + i),
CounterCurrent: uint64(1000 + i),
CounterProjected: uint64(2000 + i),
}

require.NoError(t, accounts.Update(db, &types.Account{
Layer: 1,
Address: addr,
NextNonce: accs[i].CounterCurrent,
Balance: accs[i].BalanceCurrent,
}))
}

conState.EXPECT().GetProjection(gomock.Any()).DoAndReturn(func(addr types.Address) (uint64, uint64) {
for _, acc := range accs {
if acc.Address == addr {
return acc.CounterProjected, acc.BalanceProjected
}
}
return 0, 0
}).AnyTimes()

svc := NewAccountService(db, conState)
cfg, cleanup := launchServer(t, svc)
t.Cleanup(cleanup)

conn := dialGrpc(ctx, t, cfg)
client := spacemeshv2alpha1.NewAccountServiceClient(conn)

t.Run("limit set too high", func(t *testing.T) {
_, err := client.List(ctx, &spacemeshv2alpha1.AccountRequest{Limit: 200})
require.Error(t, err)

s, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, s.Code())
require.Equal(t, "limit is capped at 100", s.Message())
})

t.Run("no limit set", func(t *testing.T) {
_, err := client.List(ctx, &spacemeshv2alpha1.AccountRequest{})
require.Error(t, err)

s, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.InvalidArgument, s.Code())
require.Equal(t, "limit must be set to <= 100", s.Message())
})

t.Run("limit and offset", func(t *testing.T) {
list, err := client.List(ctx, &spacemeshv2alpha1.AccountRequest{
Limit: 25,
Offset: 50,
})
require.NoError(t, err)
fmt.Println(list)
require.Len(t, list.Accounts, 25)
})

t.Run("all", func(t *testing.T) {
list, err := client.List(ctx, &spacemeshv2alpha1.AccountRequest{Limit: 100})
require.NoError(t, err)
require.Len(t, list.Accounts, len(accs))
})

t.Run("address", func(t *testing.T) {
list, err := client.List(ctx, &spacemeshv2alpha1.AccountRequest{
Addresses: []string{accs[0].Address.String()},
Limit: 10,
})
require.NoError(t, err)
require.Len(t, list.Accounts, 1)
require.Equal(t, accs[0].BalanceProjected, list.Accounts[0].Projected.Balance)
require.Equal(t, accs[0].CounterProjected, list.Accounts[0].Projected.Counter)
require.Equal(t, accs[0].BalanceCurrent, list.Accounts[0].Current.Balance)
require.Equal(t, accs[0].CounterCurrent, list.Accounts[0].Current.Counter)
})

t.Run("multiple addresses", func(t *testing.T) {
list, err := client.List(ctx, &spacemeshv2alpha1.AccountRequest{
Addresses: []string{accs[0].Address.String(), accs[1].Address.String()},
Limit: 10,
})
require.NoError(t, err)
require.Len(t, list.Accounts, 2)
require.Equal(t, accs[0].BalanceProjected, list.Accounts[0].Projected.Balance)
require.Equal(t, accs[0].CounterProjected, list.Accounts[0].Projected.Counter)
require.Equal(t, accs[0].BalanceCurrent, list.Accounts[0].Current.Balance)
require.Equal(t, accs[0].CounterCurrent, list.Accounts[0].Current.Counter)
require.Equal(t, accs[1].BalanceProjected, list.Accounts[1].Projected.Balance)
require.Equal(t, accs[1].CounterProjected, list.Accounts[1].Projected.Counter)
require.Equal(t, accs[1].BalanceCurrent, list.Accounts[1].Current.Balance)
require.Equal(t, accs[1].CounterCurrent, list.Accounts[1].Current.Counter)
})
}
4 changes: 4 additions & 0 deletions node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,10 @@ func (app *App) grpcService(svc grpcserver.Service, lg log.Log) (grpcserver.Serv
service := v2alpha1.NewTransactionStreamService(app.db)
app.grpcServices[svc] = service
return service, nil
case v2alpha1.Account:
service := v2alpha1.NewAccountService(app.db, app.conState)
app.grpcServices[svc] = service
return service, nil
}
return nil, fmt.Errorf("unknown service %s", svc)
}
Expand Down
Loading

0 comments on commit 0db7e0a

Please sign in to comment.