feat(accounts): Add account Create,Execute,Query implementation (#17515)

Co-authored-by: unknown unknown <unknown@unknown>
This commit is contained in:
testinginprod 2023-08-25 11:09:53 +02:00 committed by GitHub
parent 147758fd17
commit 4a536b21e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 291 additions and 0 deletions

View File

@ -0,0 +1,31 @@
package accounts
import (
"context"
"google.golang.org/protobuf/types/known/emptypb"
"cosmossdk.io/x/accounts/internal/implementation"
)
var _ implementation.Account = (*TestAccount)(nil)
type TestAccount struct{}
func (t TestAccount) RegisterInitHandler(builder *implementation.InitBuilder) {
implementation.RegisterInitHandler(builder, func(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
return &emptypb.Empty{}, nil
})
}
func (t TestAccount) RegisterExecuteHandlers(builder *implementation.ExecuteBuilder) {
implementation.RegisterExecuteHandler(builder, func(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
return &emptypb.Empty{}, nil
})
}
func (t TestAccount) RegisterQueryHandlers(builder *implementation.QueryBuilder) {
implementation.RegisterQueryHandler(builder, func(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
return &emptypb.Empty{}, nil
})
}

165
x/accounts/keeper.go Normal file
View File

@ -0,0 +1,165 @@
package accounts
import (
"context"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"cosmossdk.io/collections"
"cosmossdk.io/core/store"
"cosmossdk.io/x/accounts/internal/implementation"
)
var errAccountTypeNotFound = errors.New("account type not found")
var (
// AccountTypeKeyPrefix is the prefix for the account type key.
AccountTypeKeyPrefix = collections.NewPrefix(0)
// AccountNumberKey is the key for the account number.
AccountNumberKey = collections.NewPrefix(1)
)
func NewKeeper(ss store.KVStoreService, accounts map[string]implementation.Account) (Keeper, error) {
sb := collections.NewSchemaBuilder(ss)
keeper := Keeper{
storeService: ss,
accounts: map[string]implementation.Implementation{},
AccountNumber: collections.NewSequence(sb, AccountNumberKey, "account_number"),
AccountsByType: collections.NewMap(sb, AccountTypeKeyPrefix, "accounts_by_type", collections.BytesKey, collections.StringValue),
}
// make accounts implementation
for typ, acc := range accounts {
impl, err := implementation.NewImplementation(acc)
if err != nil {
return Keeper{}, err
}
keeper.accounts[typ] = impl
}
schema, err := sb.Build()
if err != nil {
return Keeper{}, err
}
keeper.Schema = schema
return keeper, nil
}
type Keeper struct {
storeService store.KVStoreService
accounts map[string]implementation.Implementation
// Schema is the schema for the module.
Schema collections.Schema
// AccountNumber is the last global account number.
AccountNumber collections.Sequence
// AccountsByType maps account address to their implementation.
AccountsByType collections.Map[[]byte, string]
}
// Create creates a new account of the given type.
func (k Keeper) Create(
ctx context.Context,
accountType string,
creator []byte,
initRequest any,
) (any, []byte, error) {
impl, err := k.getImplementation(accountType)
if err != nil {
return nil, nil, err
}
// make a new account address
accountAddr, err := k.makeAddress(ctx)
if err != nil {
return nil, nil, err
}
// make the context and init the account
ctx = implementation.MakeAccountContext(ctx, k.storeService, accountAddr, creator)
resp, err := impl.Init(ctx, initRequest)
if err != nil {
return nil, nil, err
}
// map account address to account type
if err := k.AccountsByType.Set(ctx, accountAddr, accountType); err != nil {
return nil, nil, err
}
return resp, accountAddr, nil
}
// Execute executes a state transition on the given account.
func (k Keeper) Execute(
ctx context.Context,
accountAddr []byte,
sender []byte,
execRequest any,
) (any, error) {
// get account type
accountType, err := k.AccountsByType.Get(ctx, accountAddr)
if err != nil {
return nil, err
}
// get account implementation
impl, err := k.getImplementation(accountType)
if err != nil {
// this means the account was initialized with an implementation
// that the chain does not know about, in theory should never happen,
// as it might signal that the app-dev stopped supporting an account type.
return nil, err
}
// make the context and execute the account state transition.
ctx = implementation.MakeAccountContext(ctx, k.storeService, accountAddr, sender)
return impl.Execute(ctx, execRequest)
}
// Query queries the given account.
func (k Keeper) Query(
ctx context.Context,
accountAddr []byte,
queryRequest any,
) (any, error) {
// get account type
accountType, err := k.AccountsByType.Get(ctx, accountAddr)
if err != nil {
return nil, err
}
// get account implementation
impl, err := k.getImplementation(accountType)
if err != nil {
// this means the account was initialized with an implementation
// that the chain does not know about, in theory should never happen,
// as it might signal that the app-dev stopped supporting an account type.
return nil, err
}
// make the context and execute the account state transition.
ctx = implementation.MakeAccountContext(ctx, k.storeService, accountAddr, nil)
return impl.Query(ctx, queryRequest)
}
func (k Keeper) getImplementation(accountType string) (implementation.Implementation, error) {
impl, ok := k.accounts[accountType]
if !ok {
return implementation.Implementation{}, fmt.Errorf("%w: %s", errAccountTypeNotFound, accountType)
}
return impl, nil
}
func (k Keeper) makeAddress(ctx context.Context) ([]byte, error) {
num, err := k.AccountNumber.Next(ctx)
if err != nil {
return nil, err
}
// TODO: better address scheme, ref: https://github.com/cosmos/cosmos-sdk/issues/17516
addr := sha256.Sum256(append([]byte("x/accounts"), binary.BigEndian.AppendUint64(nil, num)...))
return addr[:], nil
}

95
x/accounts/keeper_test.go Normal file
View File

@ -0,0 +1,95 @@
package accounts
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/emptypb"
"cosmossdk.io/collections"
"cosmossdk.io/collections/colltest"
"cosmossdk.io/x/accounts/internal/implementation"
)
func newKeeper(t *testing.T, accounts map[string]implementation.Account) (Keeper, context.Context) {
t.Helper()
ss, ctx := colltest.MockStore()
m, err := NewKeeper(ss, accounts)
require.NoError(t, err)
return m, ctx
}
func TestKeeper_Create(t *testing.T) {
m, ctx := newKeeper(t, map[string]implementation.Account{
"test": TestAccount{},
})
t.Run("ok", func(t *testing.T) {
sender := []byte("sender")
resp, addr, err := m.Create(ctx, "test", sender, &emptypb.Empty{})
require.NoError(t, err)
require.Equal(t, &emptypb.Empty{}, resp)
require.NotNil(t, addr)
// ensure acc number was increased.
num, err := m.AccountNumber.Peek(ctx)
require.NoError(t, err)
require.Equal(t, uint64(1), num)
// ensure account mapping
accType, err := m.AccountsByType.Get(ctx, addr)
require.NoError(t, err)
require.Equal(t, "test", accType)
})
t.Run("unknown account type", func(t *testing.T) {
_, _, err := m.Create(ctx, "unknown", []byte("sender"), &emptypb.Empty{})
require.ErrorIs(t, err, errAccountTypeNotFound)
})
}
func TestKeeper_Execute(t *testing.T) {
m, ctx := newKeeper(t, map[string]implementation.Account{
"test": TestAccount{},
})
// create account
sender := []byte("sender")
_, accAddr, err := m.Create(ctx, "test", sender, &emptypb.Empty{})
require.NoError(t, err)
t.Run("ok", func(t *testing.T) {
resp, err := m.Execute(ctx, accAddr, sender, &emptypb.Empty{})
require.NoError(t, err)
require.Equal(t, &emptypb.Empty{}, resp)
})
t.Run("unknown account", func(t *testing.T) {
_, err := m.Execute(ctx, []byte("unknown"), sender, &emptypb.Empty{})
require.ErrorIs(t, err, collections.ErrNotFound)
})
}
func TestKeeper_Query(t *testing.T) {
m, ctx := newKeeper(t, map[string]implementation.Account{
"test": TestAccount{},
})
// create account
sender := []byte("sender")
_, accAddr, err := m.Create(ctx, "test", sender, &emptypb.Empty{})
require.NoError(t, err)
t.Run("ok", func(t *testing.T) {
resp, err := m.Query(ctx, accAddr, &emptypb.Empty{})
require.NoError(t, err)
require.Equal(t, &emptypb.Empty{}, resp)
})
t.Run("unknown account", func(t *testing.T) {
_, err := m.Query(ctx, []byte("unknown"), &emptypb.Empty{})
require.ErrorIs(t, err, collections.ErrNotFound)
})
}