OSDN Git Service

Match fee strategy (#507)
authorPoseidon <shenao.78@163.com>
Fri, 6 Mar 2020 09:24:06 +0000 (17:24 +0800)
committerGitHub <noreply@github.com>
Fri, 6 Mar 2020 09:24:06 +0000 (17:24 +0800)
* match_fee_strategy

* rename variable

* opt code

* rename

* adjust order

* add test case

* bug fix

* bug fix

* add fee for multiple asset tesetcase

* add comment

application/mov/match/match.go
application/mov/match/match_fee.go
application/mov/match/match_test.go
application/mov/mock/mock.go
application/mov/mov_core.go

index 7b81e1a..b0eb861 100644 (file)
@@ -62,18 +62,21 @@ func (e *Engine) NextMatchedTx(tradePairs ...*common.TradePair) (*types.Tx, erro
        return tx, nil
 }
 
-func (e *Engine) addMatchTxFeeOutput(txData *types.TxData, refundAmounts, feeAmounts []*bc.AssetAmount) error {
-       for _, feeAmount := range feeAmounts {
+func (e *Engine) addMatchTxFeeOutput(txData *types.TxData, refunds []RefundAssets, fees []*bc.AssetAmount) error {
+       for _, feeAmount := range fees {
                txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(*feeAmount.AssetId, feeAmount.Amount, e.rewardProgram))
        }
 
-       for i, refundAmount := range refundAmounts {
-               contractArgs, err := segwit.DecodeP2WMCProgram(txData.Inputs[i].ControlProgram())
-               if err != nil {
-                       return err
-               }
+       for i, refund := range refunds {
+               // each trading participant may be refunded multiple assets
+               for _, assetAmount := range refund {
+                       contractArgs, err := segwit.DecodeP2WMCProgram(txData.Inputs[i].ControlProgram())
+                       if err != nil {
+                               return err
+                       }
 
-               txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(*refundAmount.AssetId, refundAmount.Amount, contractArgs.SellerProgram))
+                       txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(*assetAmount.AssetId, assetAmount.Amount, contractArgs.SellerProgram))
+               }
        }
        return nil
 }
@@ -101,13 +104,13 @@ func (e *Engine) buildMatchTx(orders []*common.Order) (*types.Tx, error) {
                txData.Inputs = append(txData.Inputs, input)
        }
 
-       receivedAmounts, priceDiff := CalcReceivedAmount(orders)
-       receivedAfterDeductFee, refundAmounts, feeAmounts := e.feeStrategy.Allocate(receivedAmounts, priceDiff)
-       if err := addMatchTxOutput(txData, orders, receivedAmounts, receivedAfterDeductFee); err != nil {
+       receivedAmounts, priceDiffs := CalcReceivedAmount(orders)
+       allocatedAssets := e.feeStrategy.Allocate(receivedAmounts, priceDiffs)
+       if err := addMatchTxOutput(txData, orders, receivedAmounts, allocatedAssets.Received); err != nil {
                return nil, err
        }
 
-       if err := e.addMatchTxFeeOutput(txData, refundAmounts, feeAmounts); err != nil {
+       if err := e.addMatchTxFeeOutput(txData, allocatedAssets.Refunds, allocatedAssets.Fees); err != nil {
                return nil, err
        }
 
@@ -165,12 +168,8 @@ func calcShouldPayAmount(receiveAmount uint64, ratioNumerator, ratioDenominator
 }
 
 // CalcReceivedAmount return amount of assets received by each participant in the matching transaction and the price difference
-func CalcReceivedAmount(orders []*common.Order) ([]*bc.AssetAmount, *bc.AssetAmount) {
-       priceDiff := &bc.AssetAmount{}
-       if len(orders) == 0 {
-               return nil, priceDiff
-       }
-
+func CalcReceivedAmount(orders []*common.Order) ([]*bc.AssetAmount, map[bc.AssetID]int64) {
+       priceDiffs := make(map[bc.AssetID]int64)
        var receivedAmounts, shouldPayAmounts []*bc.AssetAmount
        for i, order := range orders {
                requestAmount := CalcRequestAmount(order.Utxo.Amount, order.RatioNumerator, order.RatioDenominator)
@@ -184,13 +183,12 @@ func CalcReceivedAmount(orders []*common.Order) ([]*bc.AssetAmount, *bc.AssetAmo
        for i, receivedAmount := range receivedAmounts {
                oppositeShouldPayAmount := shouldPayAmounts[calcOppositeIndex(len(orders), i)]
                if oppositeShouldPayAmount.Amount > receivedAmount.Amount {
-                       priceDiff.AssetId = oppositeShouldPayAmount.AssetId
-                       priceDiff.Amount = oppositeShouldPayAmount.Amount - receivedAmount.Amount
-                       // price differential can only produce once
-                       break
+                       assetId := oppositeShouldPayAmount.AssetId
+                       amount := oppositeShouldPayAmount.Amount - receivedAmount.Amount
+                       priceDiffs[*assetId] = int64(amount)
                }
        }
-       return receivedAmounts, priceDiff
+       return receivedAmounts, priceDiffs
 }
 
 // IsMatched check does the orders can be exchange
index 6569ce1..b72260d 100644 (file)
@@ -10,22 +10,28 @@ import (
 var (
        // ErrAmountOfFeeExceedMaximum represent The fee charged is exceeded the maximum
        ErrAmountOfFeeExceedMaximum = errors.New("amount of fee greater than max fee amount")
-       // ErrFeeMoreThanOneAsset represent the fee charged can only have one asset
-       ErrFeeMoreThanOneAsset      = errors.New("fee can only be an asset")
 )
 
+// AllocatedAssets represent reallocated assets after calculating fees
+type AllocatedAssets struct {
+       Received []*bc.AssetAmount
+       Refunds  []RefundAssets
+       Fees     []*bc.AssetAmount
+}
+
+// RefundAssets represent alias for assetAmount array, because each transaction participant can be refunded multiple assets
+type RefundAssets []*bc.AssetAmount
+
 // FeeStrategy used to indicate how to charge a matching fee
 type FeeStrategy interface {
        // Allocate will allocate the price differential in matching transaction to the participants and the fee
        // @param receiveAmounts the amount of assets that the participants in the matching transaction can received when no fee is considered
-       // @param priceDiff price differential of matching transaction
-       // @return the amount of assets that the participants in the matching transaction can received when fee is considered
-       // @return the amount of assets returned to the transaction participant when the fee exceeds a certain ratio
-       // @return the amount of fees
-       Allocate(receiveAmounts []*bc.AssetAmount, priceDiff *bc.AssetAmount) ([]*bc.AssetAmount, []*bc.AssetAmount, []*bc.AssetAmount)
+       // @param priceDiffs price differential of matching transaction
+       // @return reallocated assets after calculating fees
+       Allocate(receiveAmounts []*bc.AssetAmount, priceDiffs map[bc.AssetID]int64) *AllocatedAssets
 
        // Validate verify that the fee charged for a matching transaction is correct
-       Validate(receiveAmounts []*bc.AssetAmount, priceDiff *bc.AssetAmount, feeAmounts map[bc.AssetID]int64) error
+       Validate(receiveAmounts []*bc.AssetAmount, priceDiffs, feeAmounts map[bc.AssetID]int64) error
 }
 
 // DefaultFeeStrategy represent the default fee charge strategy
@@ -39,57 +45,52 @@ func NewDefaultFeeStrategy(maxFeeRate float64) *DefaultFeeStrategy {
 }
 
 // Allocate will allocate the price differential in matching transaction to the participants and the fee
-func (d *DefaultFeeStrategy) Allocate(receiveAmounts []*bc.AssetAmount, priceDiff *bc.AssetAmount) ([]*bc.AssetAmount, []*bc.AssetAmount, []*bc.AssetAmount) {
-       receivedAfterDeductFee := make([]*bc.AssetAmount, len(receiveAmounts))
-       copy(receivedAfterDeductFee, receiveAmounts)
+func (d *DefaultFeeStrategy) Allocate(receiveAmounts []*bc.AssetAmount, priceDiffs map[bc.AssetID]int64) *AllocatedAssets {
+       var feeAmounts []*bc.AssetAmount
+       refundAmounts := make([]RefundAssets, len(receiveAmounts))
 
-       if priceDiff.Amount == 0 {
-               return receivedAfterDeductFee, nil, nil
-       }
-
-       var maxFeeAmount int64
        for _, receiveAmount := range receiveAmounts {
-               if *receiveAmount.AssetId == *priceDiff.AssetId {
-                       maxFeeAmount = calcMaxFeeAmount(receiveAmount.Amount, d.maxFeeRate)
+               if _, ok := priceDiffs[*receiveAmount.AssetId]; !ok {
+                       continue
                }
-       }
 
-       priceDiffAmount := int64(priceDiff.Amount)
-       feeAmount, reminder := priceDiffAmount, int64(0)
-       if priceDiffAmount > maxFeeAmount {
-               feeAmount = maxFeeAmount
-               reminder = priceDiffAmount - maxFeeAmount
-       }
+               priceDiff := priceDiffs[*receiveAmount.AssetId]
+               maxFeeAmount := calcMaxFeeAmount(receiveAmount.Amount, d.maxFeeRate)
 
-       // There is the remaining amount after paying the handling fee, assign it evenly to participants in the transaction
-       averageAmount := reminder / int64(len(receiveAmounts))
-       if averageAmount == 0 {
-               averageAmount = 1
-       }
+               feeAmount, reminder := priceDiff, int64(0)
+               if priceDiff > maxFeeAmount {
+                       feeAmount = maxFeeAmount
+                       reminder = priceDiff - maxFeeAmount
+               }
+
+               feeAmounts = append(feeAmounts, &bc.AssetAmount{AssetId: receiveAmount.AssetId, Amount: uint64(feeAmount)})
 
-       var refundAmounts []*bc.AssetAmount
-       for i := 0; i < len(receiveAmounts) && reminder > 0; i++ {
-               amount := averageAmount
-               if i == len(receiveAmounts)-1 {
-                       amount = reminder
+               // There is the remaining amount after paying the handling fee, assign it evenly to participants in the transaction
+               averageAmount := reminder / int64(len(receiveAmounts))
+               if averageAmount == 0 {
+                       averageAmount = 1
+               }
+
+               for i := 0; i < len(receiveAmounts) && reminder > 0; i++ {
+                       amount := averageAmount
+                       if i == len(receiveAmounts)-1 {
+                               amount = reminder
+                       }
+                       refundAmounts[i] = append(refundAmounts[i], &bc.AssetAmount{AssetId: receiveAmount.AssetId, Amount: uint64(amount)})
+                       reminder -= averageAmount
                }
-               refundAmounts = append(refundAmounts, &bc.AssetAmount{AssetId: priceDiff.AssetId, Amount: uint64(amount)})
-               reminder -= averageAmount
        }
 
-       feeAmounts := []*bc.AssetAmount{{AssetId: priceDiff.AssetId, Amount: uint64(feeAmount)}}
-       return receivedAfterDeductFee, refundAmounts, feeAmounts
+       receivedAfterDeductFee := make([]*bc.AssetAmount, len(receiveAmounts))
+       copy(receivedAfterDeductFee, receiveAmounts)
+       return &AllocatedAssets{Received: receivedAfterDeductFee, Refunds: refundAmounts, Fees: feeAmounts}
 }
 
 // Validate verify that the fee charged for a matching transaction is correct
-func (d *DefaultFeeStrategy) Validate(receiveAmounts []*bc.AssetAmount, priceDiff *bc.AssetAmount, feeAmounts map[bc.AssetID]int64) error {
-       if len(feeAmounts) > 1 {
-               return ErrFeeMoreThanOneAsset
-       }
-
+func (d *DefaultFeeStrategy) Validate(receiveAmounts []*bc.AssetAmount, feeAmounts, priceDiffs map[bc.AssetID]int64) error {
        for _, receiveAmount := range receiveAmounts {
                if feeAmount, ok := feeAmounts[*receiveAmount.AssetId]; ok {
-                       if feeAmount > calcMaxFeeAmount(receiveAmount.Amount, d.maxFeeRate) {
+                       if feeAmount > calcMaxFeeAmount(receiveAmount.Amount+uint64(priceDiffs[*receiveAmount.AssetId]), d.maxFeeRate) {
                                return ErrAmountOfFeeExceedMaximum
                        }
                }
index c14067b..ffa5398 100644 (file)
@@ -75,6 +75,16 @@ func TestGenerateMatchedTxs(t *testing.T) {
                                mock.MatchedTxs[6],
                        },
                },
+               {
+                       desc:       "multiple assets as a fee",
+                       tradePairs: []*common.TradePair{btc2eth, eth2btc},
+                       initStoreOrders: []*common.Order{
+                               mock.Btc2EthOrders[0], mock.Eth2BtcOrders[3],
+                       },
+                       wantMatchedTxs: []*types.Tx{
+                               mock.MatchedTxs[11],
+                       },
+               },
        }
 
        for i, c := range cases {
index 4a2be60..594813b 100644 (file)
@@ -104,6 +104,18 @@ var (
                                ControlProgram: MustCreateP2WMCProgram(BTC, testutil.MustDecodeHexString("0014f928b723999312df4ed51cb275a2644336c19255"), 1, 54.0),
                        },
                },
+               {
+                       FromAssetID:      &ETH,
+                       ToAssetID:        &BTC,
+                       RatioNumerator:   1,
+                       RatioDenominator: 150,
+                       Utxo: &common.MovUtxo{
+                               SourceID:       hashPtr(testutil.MustDecodeHash("82752cda63c877a8529d7a7461da6096673e45b3e0b019ce44aa18687ad20445")),
+                               SourcePos:      0,
+                               Amount:         600,
+                               ControlProgram: MustCreateP2WMCProgram(BTC, testutil.MustDecodeHexString("0014f928b723999312df4ed51cb275a2644336c19256"), 1, 150.0),
+                       },
+               },
        }
 
        Eos2EtcOrders = []*common.Order{
@@ -398,6 +410,26 @@ var (
                                types.NewIntraChainOutput(*Btc2EthOrders[0].ToAssetID, 10, RewardProgram),
                        },
                }),
+
+               // full matched transaction from Btc2EthOrders[0] Eth2BtcOrders[3]
+               types.NewTx(types.TxData{
+                       Inputs: []*types.TxInput{
+                               types.NewSpendInput([][]byte{vm.Int64Bytes(0), vm.Int64Bytes(1)}, *Btc2EthOrders[0].Utxo.SourceID, *Btc2EthOrders[0].FromAssetID, Btc2EthOrders[0].Utxo.Amount, Btc2EthOrders[0].Utxo.SourcePos, Btc2EthOrders[0].Utxo.ControlProgram),
+                               types.NewSpendInput([][]byte{vm.Int64Bytes(1), vm.Int64Bytes(1)}, *Eth2BtcOrders[3].Utxo.SourceID, *Eth2BtcOrders[3].FromAssetID, Eth2BtcOrders[3].Utxo.Amount, Eth2BtcOrders[3].Utxo.SourcePos, Eth2BtcOrders[3].Utxo.ControlProgram),
+                       },
+                       Outputs: []*types.TxOutput{
+                               types.NewIntraChainOutput(*Btc2EthOrders[0].ToAssetID, 500, testutil.MustDecodeHexString("0014f928b723999312df4ed51cb275a2644336c19251")),
+                               types.NewIntraChainOutput(*Eth2BtcOrders[3].ToAssetID, 4, testutil.MustDecodeHexString("0014f928b723999312df4ed51cb275a2644336c19256")),
+                               // fee
+                               types.NewIntraChainOutput(*Btc2EthOrders[0].ToAssetID, 25, RewardProgram),
+                               types.NewIntraChainOutput(*Eth2BtcOrders[3].ToAssetID, 1, RewardProgram),
+                               // refund
+                               types.NewIntraChainOutput(*Btc2EthOrders[0].ToAssetID, 37, testutil.MustDecodeHexString("0014f928b723999312df4ed51cb275a2644336c19251")),
+                               types.NewIntraChainOutput(*Eth2BtcOrders[3].ToAssetID, 2, testutil.MustDecodeHexString("0014f928b723999312df4ed51cb275a2644336c19251")),
+                               types.NewIntraChainOutput(*Btc2EthOrders[0].ToAssetID, 38, testutil.MustDecodeHexString("0014f928b723999312df4ed51cb275a2644336c19256")),
+                               types.NewIntraChainOutput(*Eth2BtcOrders[3].ToAssetID, 3, testutil.MustDecodeHexString("0014f928b723999312df4ed51cb275a2644336c19256")),
+                       },
+               }),
        }
 )
 
index 955a493..04cc5e7 100644 (file)
@@ -306,9 +306,9 @@ func validateMatchedTxFee(tx *types.Tx, blockHeight uint64) error {
                feeAmounts[assetID] = fee.amount
        }
 
-       receivedAmount, priceDiff := match.CalcReceivedAmount(orders)
+       receivedAmount, priceDiffs := match.CalcReceivedAmount(orders)
        feeStrategy := match.NewDefaultFeeStrategy(maxFeeRate)
-       return feeStrategy.Validate(receivedAmount, priceDiff, feeAmounts)
+       return feeStrategy.Validate(receivedAmount, feeAmounts, priceDiffs)
 }
 
 func (m *MovCore) validateMatchedTxSequence(txs []*types.Tx) error {