diff --git a/golang/cosmos/x/lien/keeper/account.go b/golang/cosmos/x/lien/keeper/account.go index 50521b9708d..06a06c664ab 100644 --- a/golang/cosmos/x/lien/keeper/account.go +++ b/golang/cosmos/x/lien/keeper/account.go @@ -6,9 +6,11 @@ import ( "github.com/Agoric/agoric-sdk/golang/cosmos/x/lien/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" vestexported "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported" + "github.com/gogo/protobuf/proto" ) // maxCoins returns coins with the maximum amount of each denomination @@ -49,28 +51,69 @@ func maxCoins(a, b sdk.Coins) sdk.Coins { return sdk.NewCoins(max...) } -// LienAccount wraps a VestingAccount to implement lien encumbrance. +// omniAccount is the full expected interface of non-module accounts. +// In addition to the methods declared in authtypes.AccountI, additional +// expectations are enforced dynamically through casting and reflection: +// +// - non-module accounts are expected to obey the GenesisAccount interface, +// i.e. to have a Validate() method; +// +// - UnpackInterfacesMessage is needed for unpacking accounts embedded +// in an Any message; +// +// - MarshalYAML() is used for String rendering; +// +// - protubuf Messages are expected to implement a number of "XXX"-prefixed +// methods not visible in the Message interface. +// +// Declaring the expected methods here allows them to implicitly fall trhough +// to an embedded omniAccount. +// +// Note that this interface will have to adapt to updated expectations in +// the cosmos-sdk or protobuf library. +type omniAccount interface { + authtypes.GenesisAccount + codectypes.UnpackInterfacesMessage + MarshalYAML() (interface{}, error) + XXX_DiscardUnknown() + XXX_Marshal([]byte, bool) ([]byte, error) + XXX_Merge(proto.Message) + XXX_Size() int + XXX_Unmarshal([]byte) error +} + +// omniVestingAccount is an omniAccount plus vesting methods. +type omniVestingAccount interface { + omniAccount + vestexported.VestingAccount +} + +// LienAccount wraps an omniVestingAccount to implement lien encumbrance. // The LockedCoins() method is the maximum of the coins locked for // liens, and the coins locked in the underlying VestingAccount. // It inherits the marshaling behavior of the wrapped account. +// In particular, the Lien account must be passed by pointer because of +// expectations from the proto library. type LienAccount struct { - vestexported.VestingAccount + omniVestingAccount lienKeeper Keeper + // messageName string } -var _ vestexported.VestingAccount = LienAccount{} +var _ vestexported.VestingAccount = &LienAccount{} +var _ authtypes.GenesisAccount = &LienAccount{} // LockedCoins implements the method from the VestingAccount interface. // It takes the maximum of the coins locked for liens and the coins // locked in the wrapped VestingAccount. -func (la LienAccount) LockedCoins(ctx sdk.Context) sdk.Coins { - wrappedLocked := la.VestingAccount.LockedCoins(ctx) +func (la *LienAccount) LockedCoins(ctx sdk.Context) sdk.Coins { + wrappedLocked := la.omniVestingAccount.LockedCoins(ctx) lienedLocked := la.LienedLockedCoins(ctx) return maxCoins(wrappedLocked, lienedLocked) } // Returns the coins which are locked for lien encumbrance. -func (la LienAccount) LienedLockedCoins(ctx sdk.Context) sdk.Coins { +func (la *LienAccount) LienedLockedCoins(ctx sdk.Context) sdk.Coins { state := la.lienKeeper.GetAccountState(ctx, la.GetAddress()) return computeLienLocked(state.Liened, state.Bonded, state.Unbonding) } @@ -86,6 +129,14 @@ func computeLienLocked(liened, bonded, unbonding sdk.Coins) sdk.Coins { return maxCoins(subtrahend, liened).Sub(subtrahend) } +// XXX_MessageName provides the message name for JSON serialization. +// See proto.MessageName(). +func (la *LienAccount) XXX_MessageName() string { + // Find the embedded account's message name for JSON + // serialization and record it for impersonation. + return proto.MessageName(la.omniVestingAccount) +} + // NewAccountWrapper returns an AccountWrapper which wraps any account // to be a LienAccount associated with the given Keeper, then unwraps // any layers that the Wrap added. @@ -95,17 +146,21 @@ func NewAccountWrapper(lk Keeper) types.AccountWrapper { if acc == nil { return nil } - return LienAccount{ - VestingAccount: makeVesting(acc), - lienKeeper: lk, + if omni, ok := acc.(omniAccount); ok { + return &LienAccount{ + omniVestingAccount: makeVesting(omni), + lienKeeper: lk, + } } + // don't wrap non-omni accounts, e.g. module accounts + return acc }, Unwrap: func(acc authtypes.AccountI) authtypes.AccountI { - if la, ok := acc.(LienAccount); ok { - acc = la.VestingAccount + if la, ok := acc.(*LienAccount); ok { + acc = la.omniVestingAccount } if uva, ok := acc.(unlockedVestingAccount); ok { - acc = uva.AccountI + acc = uva.omniAccount } return acc }, @@ -114,21 +169,22 @@ func NewAccountWrapper(lk Keeper) types.AccountWrapper { // makeVesting returns a VestingAccount, wrapping a non-vesting argument // in a trivial implementation if necessary. -func makeVesting(acc authtypes.AccountI) vestexported.VestingAccount { - if v, ok := acc.(vestexported.VestingAccount); ok { +func makeVesting(acc omniAccount) omniVestingAccount { + if v, ok := acc.(omniVestingAccount); ok { return v } - return unlockedVestingAccount{AccountI: acc} + return unlockedVestingAccount{omniAccount: acc} } -// unlockedVestingAccount extends an ordinary account to be a vesting account +// unlockedVestingAccount extends an omniAccount to be a vesting account // by simulating an original vesting amount of zero. It inherets the marshal // behavior of the wrapped account. type unlockedVestingAccount struct { - authtypes.AccountI + omniAccount } var _ vestexported.VestingAccount = unlockedVestingAccount{} +var _ authtypes.GenesisAccount = unlockedVestingAccount{} func (uva unlockedVestingAccount) LockedCoins(ctx sdk.Context) sdk.Coins { return sdk.NewCoins() @@ -167,3 +223,7 @@ func (uva unlockedVestingAccount) GetDelegatedFree() sdk.Coins { func (uva unlockedVestingAccount) GetDelegatedVesting() sdk.Coins { return sdk.NewCoins() } + +func (uva unlockedVestingAccount) XXX_MessageName() string { + return proto.MessageName(uva.omniAccount) +} diff --git a/golang/cosmos/x/lien/keeper/account_test.go b/golang/cosmos/x/lien/keeper/account_test.go index 9e77db9502f..c689da99144 100644 --- a/golang/cosmos/x/lien/keeper/account_test.go +++ b/golang/cosmos/x/lien/keeper/account_test.go @@ -85,7 +85,7 @@ func TestWrap(t *testing.T) { keeper := keeperImpl{accountKeeper: &wak} wrapper := NewAccountWrapper(keeper) wrapped := wrapper.Wrap(acc) - lienAcc, ok := wrapped.(LienAccount) + lienAcc, ok := wrapped.(*LienAccount) if !ok { t.Fatalf("wrapper did not create a lien account: %+v", wrapped) } diff --git a/golang/cosmos/x/lien/keeper/keeper.go b/golang/cosmos/x/lien/keeper/keeper.go index 293dfa1ca86..ddf85976efa 100644 --- a/golang/cosmos/x/lien/keeper/keeper.go +++ b/golang/cosmos/x/lien/keeper/keeper.go @@ -172,13 +172,11 @@ func (lk keeperImpl) getLocked(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins { if account == nil { return sdk.NewCoins() } - lienAccount, ok := account.(LienAccount) - if ok { - account = lienAccount.VestingAccount + if lienAccount, ok := account.(LienAccount); ok { + return lienAccount.omniVestingAccount.GetVestingCoins(ctx.BlockTime()) } - vestingAccount, ok := account.(vestexported.VestingAccount) - if !ok { - return sdk.NewCoins() + if vestingAccount, ok := account.(vestexported.VestingAccount); ok { + return vestingAccount.GetVestingCoins(ctx.BlockTime()) } - return vestingAccount.GetVestingCoins(ctx.BlockTime()) + return sdk.NewCoins() } diff --git a/packages/cosmic-swingset/test/test-make.js b/packages/cosmic-swingset/test/test-make.js index 23d786b3119..a7e456bee60 100644 --- a/packages/cosmic-swingset/test/test-make.js +++ b/packages/cosmic-swingset/test/test-make.js @@ -33,4 +33,13 @@ test('make and exec', async t => { resolve(); }), ); + await new Promise(resolve => + spawn('bin/ag-chain-cosmos', ['export', '--home=t1/n0'], { + cwd: `${dirname}/..`, + stdio: ['ignore', 'ignore', 'inherit'], + }).addListener('exit', code => { + t.is(code, 0, 'export exits successfully'); + resolve(); + }), + ); });