diff --git a/access/grpc/client.go b/access/grpc/client.go index 746bddae3..ed31ed45e 100644 --- a/access/grpc/client.go +++ b/access/grpc/client.go @@ -216,6 +216,22 @@ func (c *Client) GetAccountBalanceAtBlockHeight(ctx context.Context, address flo return c.grpc.GetAccountBalanceAtBlockHeight(ctx, address, blockHeight) } +func (c *Client) GetAccountKeyAtLatestBlock(ctx context.Context, address flow.Address, keyIndex uint32) (*flow.AccountKey, error) { + return c.grpc.GetAccountKeyAtLatestBlock(ctx, address, keyIndex) +} + +func (c *Client) GetAccountKeyAtBlockHeight(ctx context.Context, address flow.Address, keyIndex uint32, height uint64) (*flow.AccountKey, error) { + return c.grpc.GetAccountKeyAtBlockHeight(ctx, address, keyIndex, height) +} + +func (c *Client) GetAccountKeysAtLatestBlock(ctx context.Context, address flow.Address) ([]flow.AccountKey, error) { + return c.grpc.GetAccountKeysAtLatestBlock(ctx, address) +} + +func (c *Client) GetAccountKeysAtBlockHeight(ctx context.Context, address flow.Address, height uint64) ([]flow.AccountKey, error) { + return c.grpc.GetAccountKeysAtBlockHeight(ctx, address, height) +} + func (c *Client) ExecuteScriptAtLatestBlock(ctx context.Context, script []byte, arguments []cadence.Value) (cadence.Value, error) { return c.grpc.ExecuteScriptAtLatestBlock(ctx, script, arguments) } diff --git a/access/grpc/convert/convert.go b/access/grpc/convert/convert.go index 0c45da014..1415db5f3 100644 --- a/access/grpc/convert/convert.go +++ b/access/grpc/convert/convert.go @@ -112,6 +112,21 @@ func MessageToAccountKey(m *entities.AccountKey) (*flow.AccountKey, error) { }, nil } +func MessageToAccountKeys(m []*entities.AccountKey) ([]flow.AccountKey, error) { + var accountKeys []flow.AccountKey + + for _, entity := range m { + accountKey, err := MessageToAccountKey(entity) + if err != nil { + return nil, err + } + + accountKeys = append(accountKeys, *accountKey) + } + + return accountKeys, nil +} + func BlockToMessage(b flow.Block) (*entities.Block, error) { t := timestamppb.New(b.BlockHeader.Timestamp) diff --git a/access/grpc/grpc.go b/access/grpc/grpc.go index 9c547b25b..853a16e78 100644 --- a/access/grpc/grpc.go +++ b/access/grpc/grpc.go @@ -597,6 +597,98 @@ func (c *BaseClient) GetAccountBalanceAtBlockHeight( return response.GetBalance(), nil } +func (c *BaseClient) GetAccountKeyAtLatestBlock( + ctx context.Context, + address flow.Address, + keyIndex uint32, +) (*flow.AccountKey, error) { + request := &access.GetAccountKeyAtLatestBlockRequest{ + Address: address.Bytes(), + Index: keyIndex, + } + + response, err := c.rpcClient.GetAccountKeyAtLatestBlock(ctx, request) + if err != nil { + return nil, newRPCError(err) + } + + accountKey, err := convert.MessageToAccountKey(response.GetAccountKey()) + if err != nil { + return nil, newMessageToEntityError(entityAccount, err) + } + + return accountKey, nil +} + +func (c *BaseClient) GetAccountKeyAtBlockHeight( + ctx context.Context, + address flow.Address, + keyIndex uint32, + height uint64, +) (*flow.AccountKey, error) { + request := &access.GetAccountKeyAtBlockHeightRequest{ + Address: address.Bytes(), + Index: keyIndex, + BlockHeight: height, + } + + response, err := c.rpcClient.GetAccountKeyAtBlockHeight(ctx, request) + if err != nil { + return nil, newRPCError(err) + } + + accountKey, err := convert.MessageToAccountKey(response.GetAccountKey()) + if err != nil { + return nil, newMessageToEntityError(entityAccount, err) + } + + return accountKey, nil +} + +func (c *BaseClient) GetAccountKeysAtLatestBlock( + ctx context.Context, + address flow.Address, +) ([]flow.AccountKey, error) { + request := &access.GetAccountKeysAtLatestBlockRequest{ + Address: address.Bytes(), + } + + response, err := c.rpcClient.GetAccountKeysAtLatestBlock(ctx, request) + if err != nil { + return nil, newRPCError(err) + } + + accountKeys, err := convert.MessageToAccountKeys(response.GetAccountKeys()) + if err != nil { + return nil, newMessageToEntityError(entityAccount, err) + } + + return accountKeys, nil +} + +func (c *BaseClient) GetAccountKeysAtBlockHeight( + ctx context.Context, + address flow.Address, + height uint64, +) ([]flow.AccountKey, error) { + request := &access.GetAccountKeysAtBlockHeightRequest{ + Address: address.Bytes(), + BlockHeight: height, + } + + response, err := c.rpcClient.GetAccountKeysAtBlockHeight(ctx, request) + if err != nil { + return nil, newRPCError(err) + } + + accountKeys, err := convert.MessageToAccountKeys(response.GetAccountKeys()) + if err != nil { + return nil, newMessageToEntityError(entityAccount, err) + } + + return accountKeys, nil +} + func (c *BaseClient) ExecuteScriptAtLatestBlock( ctx context.Context, script []byte, diff --git a/access/grpc/grpc_test.go b/access/grpc/grpc_test.go index 1e9a077ff..34a7f4a51 100644 --- a/access/grpc/grpc_test.go +++ b/access/grpc/grpc_test.go @@ -939,6 +939,148 @@ func TestClient_ExecuteScriptAtLatestBlock(t *testing.T) { })) } +func TestClient_GetAccountKeyAtLatestBlock(t *testing.T) { + accounts := test.AccountGenerator() + addresses := test.AddressGenerator() + index := uint32(0) + + t.Run("Success", clientTest(func(t *testing.T, ctx context.Context, rpc *mocks.MockRPCClient, c *BaseClient) { + account := accounts.New() + response := &access.AccountKeyResponse{ + AccountKey: convert.AccountKeyToMessage(account.Keys[index]), + } + + rpc. + On("GetAccountKeyAtLatestBlock", ctx, mock.Anything). + Return(response, nil) + + key, err := c.GetAccountKeyAtLatestBlock(ctx, account.Address, index) + require.NoError(t, err) + + assert.Equal(t, account.Keys[index], key) + })) + + t.Run("Not found error", clientTest(func(t *testing.T, ctx context.Context, rpc *mocks.MockRPCClient, c *BaseClient) { + address := addresses.New() + + rpc. + On("GetAccountKeyAtLatestBlock", ctx, mock.Anything). + Return(nil, errNotFound) + + key, err := c.GetAccountKeyAtLatestBlock(ctx, address, index) + assert.Error(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) + assert.Nil(t, key) + })) +} + +func TestClient_GetAccountKeyAtBlockHeight(t *testing.T) { + accounts := test.AccountGenerator() + addresses := test.AddressGenerator() + height := uint64(42) + index := uint32(0) + + t.Run("Success", clientTest(func(t *testing.T, ctx context.Context, rpc *mocks.MockRPCClient, c *BaseClient) { + account := accounts.New() + response := &access.AccountKeyResponse{ + AccountKey: convert.AccountKeyToMessage(account.Keys[index]), + } + + rpc. + On("GetAccountKeyAtBlockHeight", ctx, mock.Anything). + Return(response, nil) + + key, err := c.GetAccountKeyAtBlockHeight(ctx, account.Address, index, height) + require.NoError(t, err) + + assert.Equal(t, account.Keys[index], key) + })) + + t.Run("Not found error", clientTest(func(t *testing.T, ctx context.Context, rpc *mocks.MockRPCClient, c *BaseClient) { + address := addresses.New() + + rpc. + On("GetAccountKeyAtBlockHeight", ctx, mock.Anything). + Return(nil, errNotFound) + + key, err := c.GetAccountKeyAtBlockHeight(ctx, address, index, height) + assert.Error(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) + assert.Nil(t, key) + })) +} + +func TestClient_GetAccountKeysAtLatestBlock(t *testing.T) { + accounts := test.AccountGenerator() + addresses := test.AddressGenerator() + index := uint32(0) + + t.Run("Success", clientTest(func(t *testing.T, ctx context.Context, rpc *mocks.MockRPCClient, c *BaseClient) { + account := accounts.New() + + response := &access.AccountKeysResponse{ + AccountKeys: []*entities.AccountKey{convert.AccountKeyToMessage(account.Keys[index])}, + } + + rpc. + On("GetAccountKeysAtLatestBlock", ctx, mock.Anything). + Return(response, nil) + + keys, err := c.GetAccountKeysAtLatestBlock(ctx, account.Address) + require.NoError(t, err) + assert.Equal(t, *account.Keys[index], keys[index]) + })) + + t.Run("Not found error", clientTest(func(t *testing.T, ctx context.Context, rpc *mocks.MockRPCClient, c *BaseClient) { + address := addresses.New() + + rpc. + On("GetAccountKeysAtLatestBlock", ctx, mock.Anything). + Return(nil, errNotFound) + + keys, err := c.GetAccountKeysAtLatestBlock(ctx, address) + assert.Error(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) + assert.Empty(t, keys) + })) +} + +func TestClient_GetAccountKeysAtBlockHeight(t *testing.T) { + accounts := test.AccountGenerator() + addresses := test.AddressGenerator() + height := uint64(42) + index := uint32(0) + + t.Run("Success", clientTest(func(t *testing.T, ctx context.Context, rpc *mocks.MockRPCClient, c *BaseClient) { + account := accounts.New() + + response := &access.AccountKeysResponse{ + AccountKeys: []*entities.AccountKey{convert.AccountKeyToMessage(account.Keys[index])}, + } + + rpc. + On("GetAccountKeysAtBlockHeight", ctx, mock.Anything). + Return(response, nil) + + keys, err := c.GetAccountKeysAtBlockHeight(ctx, account.Address, height) + require.NoError(t, err) + assert.Equal(t, *account.Keys[index], keys[index]) + })) + + t.Run("Not found error", clientTest(func(t *testing.T, ctx context.Context, rpc *mocks.MockRPCClient, c *BaseClient) { + address := addresses.New() + + rpc. + On("GetAccountKeysAtBlockHeight", ctx, mock.Anything). + Return(nil, errNotFound) + + keys, err := c.GetAccountKeysAtBlockHeight(ctx, address, height) + assert.Error(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) + assert.Empty(t, keys) + })) +} + func TestClient_ExecuteScriptAtBlockID(t *testing.T) { ids := test.IdentifierGenerator()