diff --git a/context.go b/context.go index 2ba181c56..a7737d5f5 100644 --- a/context.go +++ b/context.go @@ -1,6 +1,7 @@ package basecoin import ( + wire "github.com/tendermint/go-wire" "github.com/tendermint/go-wire/data" "github.com/tendermint/tmlibs/log" ) @@ -20,6 +21,10 @@ func NewActor(app string, addr []byte) Actor { return Actor{App: app, Address: addr} } +func (a Actor) Bytes() []byte { + return wire.BinaryBytes(a) +} + // Context is an interface, so we can implement "secure" variants that // rely on private fields to control the actions type Context interface { diff --git a/errors/common.go b/errors/common.go index 8eb05045d..2f74c8c13 100644 --- a/errors/common.go +++ b/errors/common.go @@ -20,6 +20,7 @@ const ( msgInvalidSequence = "Invalid Sequence" msgInvalidSignature = "Invalid Signature" msgInsufficientFees = "Insufficient Fees" + msgInsufficientFunds = "Insufficient Funds" msgNoInputs = "No Input Coins" msgNoOutputs = "No Output Coins" msgTooLarge = "Input size too large" @@ -88,6 +89,10 @@ func InsufficientFees() TMError { return New(msgInsufficientFees, abci.CodeType_BaseInvalidInput) } +func InsufficientFunds() TMError { + return New(msgInsufficientFunds, abci.CodeType_BaseInvalidInput) +} + func NoInputs() TMError { return New(msgNoInputs, abci.CodeType_BaseInvalidInput) } diff --git a/modules/coin/store.go b/modules/coin/store.go new file mode 100644 index 000000000..2b217d4f0 --- /dev/null +++ b/modules/coin/store.go @@ -0,0 +1,95 @@ +package coin + +import ( + "fmt" + + wire "github.com/tendermint/go-wire" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/errors" + "github.com/tendermint/basecoin/types" +) + +type Accountant struct { + Prefix []byte +} + +func (a Accountant) GetAccount(store types.KVStore, addr basecoin.Actor) (Account, error) { + // TODO: how to handle empty accounts?? + return loadAccount(store, a.makeKey(addr)) +} + +// CheckCoins makes sure there are funds, but doesn't change anything +func (a Accountant) CheckCoins(store types.KVStore, addr basecoin.Actor, coins types.Coins, seq int) (types.Coins, error) { + acct, err := a.updateCoins(store, addr, coins, seq) + return acct.Coins, err +} + +// ChangeCoins changes the money, returns error if it would be negative +func (a Accountant) ChangeCoins(store types.KVStore, addr basecoin.Actor, coins types.Coins, seq int) (types.Coins, error) { + acct, err := a.updateCoins(store, addr, coins, seq) + if err != nil { + return acct.Coins, err + } + + err = storeAccount(store, a.makeKey(addr), acct) + return acct.Coins, err +} + +// updateCoins will load the account, make all checks, and return the updated account. +// +// it doesn't save anything, that is up to you to decide (Check/Change Coins) +func (a Accountant) updateCoins(store types.KVStore, addr basecoin.Actor, coins types.Coins, seq int) (acct Account, err error) { + acct, err = loadAccount(store, a.makeKey(addr)) + if err != nil { + return acct, err + } + + // check sequence + if seq != acct.Sequence+1 { + return acct, errors.InvalidSequence() + } + acct.Sequence += 1 + + // check amount + final := acct.Coins.Minus(coins) + if !final.IsNonnegative() { + return acct, errors.InsufficientFunds() + } + + acct.Coins = final + return acct, nil +} + +func (a Accountant) makeKey(addr basecoin.Actor) []byte { + key := addr.Bytes() + if len(a.Prefix) > 0 { + key = append(a.Prefix, key...) + } + return key +} + +type Account struct { + Coins types.Coins `json:"coins"` + Sequence int `json:"seq"` +} + +func loadAccount(store types.KVStore, key []byte) (acct Account, err error) { + data := store.Get(key) + if len(data) == 0 { + // TODO: error or empty???? + return acct, errors.InternalError("No account found") + } + err = wire.ReadBinaryBytes(data, &acct) + if err != nil { + msg := fmt.Sprintf("Error reading account %X", key) + return acct, errors.InternalError(msg) + } + return acct, nil +} + +func storeAccount(store types.KVStore, key []byte, acct Account) error { + bin := wire.BinaryBytes(acct) + store.Set(key, bin) + return nil // real stores can return error... +}