diff --git a/cmd/bcpd/app/app.go b/cmd/bcpd/app/app.go index b305fcee..1b1a4dfc 100644 --- a/cmd/bcpd/app/app.go +++ b/cmd/bcpd/app/app.go @@ -48,12 +48,9 @@ func Chain(authFn x.Authenticator) app.Decorators { utils.NewSavepoint().OnCheck(), sigs.NewDecorator(), multisig.NewDecorator(authFn), - cash.NewFeeDecorator(authFn, ctrl), + cash.NewDynamicFeeDecorator(authFn, ctrl), // cannot pay for fee with hashlock... hashlock.NewDecorator(), - // on DeliverTx, bad tx will increment nonce and take fee - // even if the message fails - utils.NewSavepoint().OnDeliver(), // make sure we execute all the transactions in batch after savepoint batch.NewDecorator(), ) diff --git a/cmd/bcpd/app/app_test.go b/cmd/bcpd/app/app_test.go index d337d5d2..65d5d47f 100644 --- a/cmd/bcpd/app/app_test.go +++ b/cmd/bcpd/app/app_test.go @@ -1,7 +1,6 @@ package app import ( - "bytes" "encoding/binary" "encoding/hex" "encoding/json" @@ -228,7 +227,7 @@ func withWalletAppState(t require.TestingT, accounts []*account) string { Gconf map[string]interface{} `json:"gconf"` }{ Gconf: map[string]interface{}{ - cash.GconfCollectorAddress: "fake-collector-address", + cash.GconfCollectorAddress: "66616b652d636f6c6c6563746f722d61646472657373", cash.GconfMinimalFee: coin.Coin{}, // no fee }, } @@ -272,38 +271,37 @@ func (c *contract) signers() []*account { } func appStateGenesis(t require.TestingT, contracts []*contract) string { - var buff bytes.Buffer - for i, acc := range contracts { - _, err := buff.WriteString(fmt.Sprintf(`{ - "name": "wallet%d", - "address": "%X", - "coins": [{ - "whole": 50000, - "ticker": "ETH" - },{ - "whole": 1234, - "ticker": "FRNK" - }] - },`, i, acc.address())) - require.NoError(t, err) + type dt map[string]interface{} + type arr []interface{} + + var wallets arr + for i, c := range contracts { + wallets = append(wallets, dt{ + "name": fmt.Sprintf("wallet%d", i), + "address": c.address(), + "coins": arr{ + dt{"whole": 50000, "ticker": "ETH"}, + dt{"whole": 1234, "ticker": "FRNK"}, + }, + }) } - walletStr := buff.String() - walletStr = walletStr[:len(walletStr)-1] - appState := fmt.Sprintf(`{ - "wallets": [%s], - "currencies": [{ - "ticker": "ETH", - "name": "Smells like ethereum", - "sig_figs": 9 - },{ - "ticker": "FRNK", - "name": "Frankie", - "sig_figs": 3 - }] - }`, walletStr) - - return appState + state := dt{ + "wallets": wallets, + "currencies": arr{ + dt{"ticker": "ETH", "name": "Smells like ethereum", "sig_figs": 9}, + dt{"ticker": "FRNK", "name": "Frankie", "sig_figs": 3}, + }, + "gconf": dt{ + "cash:collector_address": "66616b652d636f6c6c6563746f722d61646472657373", + }, + } + + b, err := json.MarshalIndent(state, "", "\t") + if err != nil { + panic(err) + } + return string(b) } // sendToken creates the transaction, signs it and sends it @@ -392,8 +390,7 @@ func sendBatch(t require.TestingT, fail bool, baseApp app.BaseApp, chainID strin // createContract creates an immutable contract, signs the transaction and sends it // checks contract has been created correctly -func createContract(t require.TestingT, baseApp app.BaseApp, chainID string, height int64, signers []*account, - activationThreshold int64, contractSigs ...[]byte) []byte { +func createContract(t require.TestingT, baseApp app.BaseApp, chainID string, height int64, signers []*account, activationThreshold int64, contractSigs ...[]byte) []byte { msg := &multisig.CreateContractMsg{ Sigs: contractSigs, ActivationThreshold: activationThreshold, @@ -420,8 +417,7 @@ func createContract(t require.TestingT, baseApp app.BaseApp, chainID string, hei // signAndCommit signs tx with signatures from signers and submits to the chain // asserts and fails the test in case of errors during the process -func signAndCommit(t require.TestingT, fail bool, app app.BaseApp, tx *Tx, signers []*account, chainID string, - height int64) abci.ResponseDeliverTx { +func signAndCommit(t require.TestingT, fail bool, app app.BaseApp, tx *Tx, signers []*account, chainID string, height int64) abci.ResponseDeliverTx { for _, signer := range signers { sig, err := sigs.SignTx(signer.pk, tx, chainID, signer.nonce()) require.NoError(t, err) diff --git a/cmd/bnsd/app/app.go b/cmd/bnsd/app/app.go index 9b5fe96e..58a88276 100644 --- a/cmd/bnsd/app/app.go +++ b/cmd/bnsd/app/app.go @@ -50,15 +50,12 @@ func Chain(authFn x.Authenticator) app.Decorators { utils.NewSavepoint().OnCheck(), sigs.NewDecorator(), multisig.NewDecorator(authFn), - cash.NewFeeDecorator(authFn, ctrl), + cash.NewDynamicFeeDecorator(authFn, ctrl), // cannot pay for fee with hashlock... hashlock.NewDecorator(), // batch commented out temporarily to minimize release features // make sure we execute all the transactions in batch before the save point //batch.NewDecorator(), - // on DeliverTx, bad tx will increment nonce and take fee - // even if the message fails - utils.NewSavepoint().OnDeliver(), ) } diff --git a/cmd/bnsd/app/testdata/fixtures/app.go b/cmd/bnsd/app/testdata/fixtures/app.go index 597574e6..8b46231c 100644 --- a/cmd/bnsd/app/testdata/fixtures/app.go +++ b/cmd/bnsd/app/testdata/fixtures/app.go @@ -91,7 +91,7 @@ func appStateGenesis(keyAddress weave.Address) []byte { }, }, Gconf: map[string]interface{}{ - cash.GconfCollectorAddress: "fake-collector-address", + cash.GconfCollectorAddress: "66616b652d636f6c6c6563746f722d61646472657373", cash.GconfMinimalFee: coin.Coin{Whole: 0}, // no fee }, } diff --git a/examples/mycoind/app/app_test.go b/examples/mycoind/app/app_test.go index ea79b4d0..49a5fbba 100644 --- a/examples/mycoind/app/app_test.go +++ b/examples/mycoind/app/app_test.go @@ -38,7 +38,7 @@ func testInitChain(t *testing.T, myApp app.BaseApp, addr string) { }, }, "gconf": dict{ - cash.GconfCollectorAddress: "fake-collector-address", + cash.GconfCollectorAddress: "66616b652d636f6c6c6563746f722d61646472657373", cash.GconfMinimalFee: coin.Coin{Whole: 0}, // no fee }, }) diff --git a/examples/mycoind/app/init.go b/examples/mycoind/app/init.go index 39da1ec4..7550bf77 100644 --- a/examples/mycoind/app/init.go +++ b/examples/mycoind/app/init.go @@ -56,7 +56,7 @@ func GenInitOptions(args []string) (json.RawMessage, error) { }, }, "gconf": dict{ - cash.GconfCollectorAddress: "fake-collector-address", + cash.GconfCollectorAddress: "66616b652d636f6c6c6563746f722d61646472657373", cash.GconfMinimalFee: coin.Coin{Whole: 0}, // no fee }, }) diff --git a/x/cash/controller.go b/x/cash/controller.go index a1e3be6d..dc6e8e55 100644 --- a/x/cash/controller.go +++ b/x/cash/controller.go @@ -6,13 +6,18 @@ import ( "github.com/iov-one/weave/errors" ) +// CoinsMover is an interface for moving coins between accounts. +type CoinMover interface { + // Moving coins must happen from the source to the destination address. + // Zero or negative values must result in an error. + MoveCoins(store weave.KVStore, src weave.Address, dest weave.Address, amount coin.Coin) error +} + // Controller is the functionality needed by cash.Handler and cash.Decorator. // BaseController should work plenty fine, but you can add other logic if so // desired type Controller interface { - // MoveCoins removes funds from the source account and adds them to the - // destination account. This operation is atomic. - MoveCoins(store weave.KVStore, src weave.Address, dest weave.Address, amount coin.Coin) error + CoinMover // IssueCoins increase the number of funds on given accouunt by a // specified amount. @@ -54,6 +59,9 @@ func (c BaseController) Balance(store weave.KVStore, src weave.Address) (coin.Co func (c BaseController) MoveCoins(store weave.KVStore, src weave.Address, dest weave.Address, amount coin.Coin) error { + if amount.IsZero() { + return errors.ErrInvalidAmount.New("zero value") + } if !amount.IsPositive() { return errors.ErrInvalidAmount.Newf("non-positive SendMsg: %#v", &amount) } diff --git a/x/cash/dynamicfee.go b/x/cash/dynamicfee.go new file mode 100644 index 00000000..63dcdd29 --- /dev/null +++ b/x/cash/dynamicfee.go @@ -0,0 +1,192 @@ +/* + +DynamicFeeDecorator is an enhanced version the basic FeeDecorator with better +handling of transaction errors and ability to deduct/enforce app-specific fees. + +The business logic is: +1. If a transaction fee < min fee, or a transaction fee cannot be paid, reject + it with an error. +2. Run the transaction. +3. If a transaction processing results in an error, revert all transaction + changes and charge only the min fee. + +TODO: If a transaction succeeded, but requested a RequiredFee higher than paid +fee, revert all transaction changes and refund all but the min fee, returning +an error. + +If a transaction succeeded, and at least RequiredFee was paid, everything is +committed and we return success + +It also embeds a checkpoint inside, so in the typical application stack: + + cash.NewFeeDecorator(authFn, ctrl), + utils.NewSavepoint().OnDeliver(), + +can be replaced by + + cash.NewDynamicFeeDecorator(authFn, ctrl), + +As with FeeDecorator, all deducted fees are send to the collector, whose +address is configured via gconf package. + +*/ + +package cash + +import ( + "github.com/iov-one/weave" + coin "github.com/iov-one/weave/coin" + "github.com/iov-one/weave/errors" + "github.com/iov-one/weave/gconf" + "github.com/iov-one/weave/x" +) + +type DynamicFeeDecorator struct { + auth x.Authenticator + ctrl CoinMover +} + +var _ weave.Decorator = DynamicFeeDecorator{} + +// NewDynamicFeeDecorator returns a DynamicFeeDecorator with the given +// minimum fee, and all collected fees going to a default address. +func NewDynamicFeeDecorator(auth x.Authenticator, ctrl Controller) DynamicFeeDecorator { + return DynamicFeeDecorator{ + auth: auth, + ctrl: ctrl, + } +} + +// Check verifies and deducts fees before calling down the stack +func (d DynamicFeeDecorator) Check(ctx weave.Context, store weave.KVStore, tx weave.Tx, next weave.Checker) (res weave.CheckResult, ferr error) { + fee, payer, cache, err := d.prepare(ctx, store, tx) + if err != nil { + return res, errors.Wrap(err, "cannot prepare") + } + + defer func() { + if ferr == nil { + cache.Write() + res.GasPayment += toPayment(fee) + } else { + cache.Discard() + _ = d.chargeMinimalFee(store, payer) + } + }() + + if err := d.chargeFee(cache, payer, fee); err != nil { + return res, errors.Wrap(err, "cannot charge fee") + } + return next.Check(ctx, cache, tx) +} + +// Deliver verifies and deducts fees before calling down the stack +func (d DynamicFeeDecorator) Deliver(ctx weave.Context, store weave.KVStore, tx weave.Tx, next weave.Deliverer) (res weave.DeliverResult, ferr error) { + fee, payer, cache, err := d.prepare(ctx, store, tx) + if err != nil { + return res, errors.Wrap(err, "cannot prepare") + } + + defer func() { + if ferr == nil { + cache.Write() + } else { + cache.Discard() + _ = d.chargeMinimalFee(store, payer) + } + }() + + if err := d.chargeFee(cache, payer, fee); err != nil { + return weave.DeliverResult{}, errors.Wrap(err, "cannot charge fee") + } + return next.Deliver(ctx, cache, tx) +} + +func (d DynamicFeeDecorator) chargeFee(store weave.KVStore, src weave.Address, amount coin.Coin) error { + if amount.IsZero() { + return nil + } + dest := gconf.Address(store, GconfCollectorAddress) + return d.ctrl.MoveCoins(store, src, dest, amount) +} + +// chargeMinimalFee deduct an anty span fee from a given account. +func (d DynamicFeeDecorator) chargeMinimalFee(store weave.KVStore, src weave.Address) error { + fee := gconf.Coin(store, GconfMinimalFee) + if fee.IsZero() { + return nil + } + if fee.Ticker == "" { + return errors.ErrHuman.New("minimal fee without a ticker") + } + return d.chargeFee(store, src, fee) +} + +// prepare is all shared setup between Check and Deliver. It computes the fee +// for the transaction, ensures that the payer is authenticated and prepares +// the database transaction. +func (d DynamicFeeDecorator) prepare(ctx weave.Context, store weave.KVStore, tx weave.Tx) (fee coin.Coin, payer weave.Address, cache weave.KVCacheWrap, err error) { + finfo, err := d.extractFee(ctx, tx, store) + if err != nil { + return fee, payer, cache, errors.Wrap(err, "cannot extract fee") + } + // Dererefence the fees (handling nil). + if pfee := finfo.GetFees(); pfee != nil { + fee = *pfee + } + payer = finfo.GetPayer() + + // Verify we have access to the money. + if !d.auth.HasAddress(ctx, payer) { + err := errors.ErrUnauthorized.New("fee payer signature missing") + return fee, payer, cache, err + } + + // Ensure we can execute subtransactions (see check on utils.Savepoint). + cstore, ok := store.(weave.CacheableKVStore) + if !ok { + err = errors.ErrInternal.New("need cachable kvstore") + return fee, payer, cache, err + } + cache = cstore.CacheWrap() + return fee, payer, cache, nil +} + +// this returns the fee info to deduct and the error if incorrectly set +func (d DynamicFeeDecorator) extractFee(ctx weave.Context, tx weave.Tx, store weave.KVStore) (*FeeInfo, error) { + var finfo *FeeInfo + ftx, ok := tx.(FeeTx) + if ok { + payer := x.MainSigner(ctx, d.auth).Address() + finfo = ftx.GetFees().DefaultPayer(payer) + } + + txFee := finfo.GetFees() + if coin.IsEmpty(txFee) { + minFee := gconf.Coin(store, GconfMinimalFee) + if minFee.IsZero() { + return finfo, nil + } + return nil, errors.ErrInsufficientAmount.New("zero transaction fee is not allowed") + } + + if err := finfo.Validate(); err != nil { + return nil, errors.Wrap(err, "invalid fee") + } + + minFee := gconf.Coin(store, GconfMinimalFee) + if minFee.IsZero() { + return finfo, nil + } + if minFee.Ticker == "" { + return nil, errors.ErrHuman.New("minumal fee curency not set") + } + if !txFee.SameType(minFee) { + return nil, coin.ErrInvalidCurrency.Newf("min fee is %s and tx fee is %s", minFee.Ticker, txFee.Ticker) + + } + if !txFee.IsGTE(minFee) { + return nil, errors.ErrInsufficientAmount.Newf("transaction fee less than minimum: %v", txFee) + } + return finfo, nil +} diff --git a/x/cash/dynamicfee_test.go b/x/cash/dynamicfee_test.go new file mode 100644 index 00000000..40cdc4e3 --- /dev/null +++ b/x/cash/dynamicfee_test.go @@ -0,0 +1,244 @@ +package cash + +import ( + "testing" + + "github.com/iov-one/weave" + coin "github.com/iov-one/weave/coin" + "github.com/iov-one/weave/errors" + "github.com/iov-one/weave/gconf" + "github.com/iov-one/weave/orm" + "github.com/iov-one/weave/store" + "github.com/iov-one/weave/x" +) + +func TestDynamicFeeDecorator(t *testing.T) { + perm1 := weave.NewCondition("sigs", "ed25519", []byte{1, 2, 3}) + //perm2 := weave.NewCondition("sigs", "ed25519", []byte{3, 4, 5}) + perm3 := weave.NewCondition("custom", "type", []byte{0xAB}) + + collectorAddr := perm3.Address() + + coinp := func(w, f int64, t string) *coin.Coin { + c := coin.NewCoin(w, f, t) + return &c + } + + walletObj := func(a weave.Address, w, f int64, ticker string) orm.Object { + t.Helper() + obj, err := WalletWith(a, coinp(w, f, ticker)) + if err != nil { + t.Fatalf("cannot create a wallet: %s", err) + } + return obj + } + + cases := map[string]struct { + signers []weave.Condition + handler *handlerMock + minimumFee coin.Coin + txFee coin.Coin + // Wallet state created before running Check + initWallets []orm.Object + // Wallet state applied after running Check but before running Deliver + updateWallets []orm.Object + + wantCheckErr error + wantCheckTxFee coin.Coin + wantDeliverErr error + wantDeliverTxFee coin.Coin + wantGasPayment int64 + }{ + "on success full transaction fee is charged": { + signers: []weave.Condition{perm1}, + handler: &handlerMock{}, + initWallets: []orm.Object{ + walletObj(perm1.Address(), 1, 0, "BTC"), + }, + minimumFee: coin.NewCoin(0, 23, "BTC"), + txFee: coin.NewCoin(0, 421, "BTC"), + wantCheckTxFee: coin.NewCoin(0, 421, "BTC"), + wantDeliverTxFee: coin.NewCoin(0, 421, "BTC"), + wantGasPayment: 421, + }, + "on a handler check failure minimum fee is charged": { + signers: []weave.Condition{perm1}, + handler: &handlerMock{checkErr: ErrTestingError}, + initWallets: []orm.Object{ + walletObj(perm1.Address(), 1, 0, "BTC"), + }, + minimumFee: coin.NewCoin(0, 23, "BTC"), + txFee: coin.NewCoin(0, 421, "BTC"), + wantCheckErr: ErrTestingError, + wantCheckTxFee: coin.NewCoin(0, 23, "BTC"), + }, + "on insufficient fee funds minimum fee is charged": { + signers: []weave.Condition{perm1}, + initWallets: []orm.Object{ + walletObj(perm1.Address(), 0, 100, "BTC"), + }, + minimumFee: coin.NewCoin(0, 23, "BTC"), + txFee: coin.NewCoin(0, 421, "BTC"), // Wallet has not enough. + wantCheckErr: errors.ErrInsufficientAmount, + wantCheckTxFee: coin.NewCoin(0, 23, "BTC"), + }, + "on inssuficient funds minimum fee withdraw fails": { + signers: []weave.Condition{perm1}, + initWallets: []orm.Object{ + walletObj(perm1.Address(), 0, 1, "BTC"), + }, + minimumFee: coin.NewCoin(0, 23, "BTC"), // Wallet has not enough. + txFee: coin.NewCoin(0, 421, "BTC"), // Wallet has not enough. + wantCheckErr: errors.ErrInsufficientAmount, + wantCheckTxFee: coin.Coin{}, + }, + "on transaction fee ticker mismatch minimum fee with no currency accepts anything": { + signers: []weave.Condition{perm1}, + initWallets: []orm.Object{ + walletObj(perm1.Address(), 1, 0, "BTC"), + }, + minimumFee: coin.NewCoin(0, 23, ""), + txFee: coin.NewCoin(0, 421, "ETH"), + wantCheckErr: errors.ErrHuman, + }, + "on a handler deliver failure only minimum fee is charged": { + signers: []weave.Condition{perm1}, + handler: &handlerMock{deliverErr: ErrTestingError}, + initWallets: []orm.Object{ + walletObj(perm1.Address(), 1, 0, "BTC"), + }, + minimumFee: coin.NewCoin(0, 11, "BTC"), + txFee: coin.NewCoin(0, 44, "BTC"), + wantGasPayment: 44, // This assimes that transaction fee was charged. + wantCheckTxFee: coin.NewCoin(0, 44, "BTC"), + wantDeliverErr: ErrTestingError, + wantDeliverTxFee: coin.NewCoin(0, 11, "BTC"), + }, + } + + for testName, tc := range cases { + t.Run(testName, func(t *testing.T) { + auth := helpers.Authenticate(tc.signers...) + bucket := NewBucket() + ctrl := NewController(bucket) + h := NewDynamicFeeDecorator(auth, ctrl) + + tx := &txMock{info: &FeeInfo{Fees: &tc.txFee}} + + db := store.MemStore() + + gconf.SetValue(db, GconfCollectorAddress, collectorAddr) + gconf.SetValue(db, GconfMinimalFee, tc.minimumFee) + + ensureWallets(t, db, tc.initWallets) + + cache := db.CacheWrap() + + cRes, err := h.Check(nil, cache, tx, tc.handler) + if !errors.Is(tc.wantCheckErr, err) { + t.Fatalf("got check error: %v", err) + } + if tc.wantGasPayment != cRes.GasPayment { + t.Errorf("gas payment: %d", cRes.GasPayment) + } + + assertCharged(t, cache, ctrl, tc.wantCheckTxFee) + + ensureWallets(t, cache, tc.updateWallets) + + // If the check failed, deliver must not be called. + if tc.wantCheckErr != nil { + return + } + + cache.Discard() + + if _, err = h.Deliver(nil, cache, tx, tc.handler); !errors.Is(tc.wantDeliverErr, err) { + t.Fatalf("got deliver error: %v", err) + } + + assertCharged(t, cache, ctrl, tc.wantDeliverTxFee) + }) + } +} + +var helpers x.TestHelpers + +// ensureWallets persist state of given wallet objects in the database. If +// a wallet already exist it is overwritten. +func ensureWallets(t *testing.T, db weave.KVStore, wallets []orm.Object) { + t.Helper() + + bucket := NewBucket() + for i, w := range wallets { + if err := bucket.Save(db, w); err != nil { + t.Fatalf("cannot set %d wallet: %s", i, err) + } + } +} + +// assertCharged check that given account was charged according to the fee +// configuration. +func assertCharged(t *testing.T, db weave.KVStore, ctrl Controller, want coin.Coin) { + t.Helper() + + minimumFee := gconf.Coin(db, GconfMinimalFee) + collectorAddr := gconf.Address(db, GconfCollectorAddress) + + switch chargedFee, err := ctrl.Balance(db, collectorAddr); { + case err == nil: + wantTx := coin.Coins{&want} + if !wantTx.Equals(chargedFee) { + t.Errorf("charged fee: %v", chargedFee) + } + case errors.Is(errors.ErrNotFound, err): + if minimumFee.IsZero() { + // Minimal fee is zero so the collector account is zero + // as well (not even created). All good. + } else { + if want.IsZero() { + // This is a weird case when a transaction was + // submitted but the signer does not have + // enough funds to pay the minimum (anty spam) + // fee. + } else { + t.Error("no fee charged") + } + } + default: + t.Errorf("cannot check collector account balance: %s", err) + } +} + +type txMock struct { + weave.Tx + FeeTx + info *FeeInfo +} + +func (m *txMock) GetFees() *FeeInfo { + return m.info +} + +// Declare a unique error that can be matched in tests. This error is declared +// only in tests so there is no way it can be returned by the implementation by +// an accident. +var ErrTestingError = errors.Register(123456789, "testing error") + +type handlerMock struct { + checkRes weave.CheckResult + checkErr error + + deliverRes weave.DeliverResult + deliverErr error +} + +var _ weave.Handler = (*handlerMock)(nil) + +func (m *handlerMock) Check(weave.Context, weave.KVStore, weave.Tx) (weave.CheckResult, error) { + return m.checkRes, m.checkErr +} + +func (m *handlerMock) Deliver(weave.Context, weave.KVStore, weave.Tx) (weave.DeliverResult, error) { + return m.deliverRes, m.deliverErr +} diff --git a/x/cash/decorator.go b/x/cash/staticfee.go similarity index 77% rename from x/cash/decorator.go rename to x/cash/staticfee.go index 946a81d4..6a86807b 100644 --- a/x/cash/decorator.go +++ b/x/cash/staticfee.go @@ -1,3 +1,18 @@ +/* + +FeeDecorator ensures that the fee can be deducted from the account. All +deducted fees are send to the collector, which can be set to an address +controlled by another extension ("smart contract"). +Collector address is configured via gconf package. + +Minimal fee is configured via gconf package. If minimal is zero, no fees +required, but will speed processing. If a currency is set on minimal fee, then +all fees must be paid in that currency + +It uses auth to verify the sender. + +*/ + package cash import ( @@ -8,25 +23,9 @@ import ( "github.com/iov-one/weave/x" ) -//----------------- FeeDecorator ---------------- -// -// This is just a binding from the functionality into the -// Application stack, not much business logic here. - -// FeeDecorator ensures that the fee can be deducted from -// the account. All deducted fees are send to the collector, -// which can be set to an address controlled by another -// extension ("smart contract"). -// Collector address is configured via gconf package. -// -// Minimal fee is configured via gconf package. If minimal is zero, no fees -// required, but will speed processing. If a currency is set on minimal fee, -// then all fees must be paid in that currency -// -// It uses auth to verify the sender type FeeDecorator struct { - auth x.Authenticator - control Controller + auth x.Authenticator + ctrl CoinMover } const ( @@ -39,10 +38,10 @@ var _ weave.Decorator = FeeDecorator{} // NewFeeDecorator returns a FeeDecorator with the given // minimum fee, and all collected fees going to a // default address. -func NewFeeDecorator(auth x.Authenticator, control Controller) FeeDecorator { +func NewFeeDecorator(auth x.Authenticator, ctrl CoinMover) FeeDecorator { return FeeDecorator{ - auth: auth, - control: control, + auth: auth, + ctrl: ctrl, } } @@ -68,7 +67,7 @@ func (d FeeDecorator) Check(ctx weave.Context, store weave.KVStore, tx weave.Tx, } // and have enough collector := gconf.Address(store, GconfCollectorAddress) - err = d.control.MoveCoins(store, finfo.Payer, collector, *fee) + err = d.ctrl.MoveCoins(store, finfo.Payer, collector, *fee) if err != nil { return res, err } @@ -102,7 +101,7 @@ func (d FeeDecorator) Deliver(ctx weave.Context, store weave.KVStore, tx weave.T } // and subtract it from the account collector := gconf.Address(store, GconfCollectorAddress) - err = d.control.MoveCoins(store, finfo.Payer, collector, *fee) + err = d.ctrl.MoveCoins(store, finfo.Payer, collector, *fee) if err != nil { return res, err } diff --git a/x/cash/decorator_test.go b/x/cash/staticfee_test.go similarity index 100% rename from x/cash/decorator_test.go rename to x/cash/staticfee_test.go