OSDN Git Service

Merge branch 'mov_crossin_tx' of https://github.com/Bytom/vapor into mov_crossin_tx
authorwz <mars@bytom.io>
Wed, 23 Oct 2019 11:38:45 +0000 (19:38 +0800)
committerwz <mars@bytom.io>
Wed, 23 Oct 2019 11:38:45 +0000 (19:38 +0800)
application/mov/contract/contract.go [new file with mode: 0644]
application/mov/match/match.go [new file with mode: 0644]
application/mov/match/match_test.go [new file with mode: 0644]
math/algorithm.go [new file with mode: 0644]

diff --git a/application/mov/contract/contract.go b/application/mov/contract/contract.go
new file mode 100644 (file)
index 0000000..770def5
--- /dev/null
@@ -0,0 +1,34 @@
+package contract
+
+import (
+       "encoding/hex"
+
+       "github.com/vapor/protocol/bc/types"
+       "github.com/vapor/protocol/vm"
+)
+
+const (
+       sizeOfCancelClauseArgs = 3
+       sizeOfPartialTradeClauseArgs = 3
+       sizeOfFullTradeClauseArgs = 2
+
+       PartialTradeClauseSelector int64 = iota
+       FullTradeClauseSelector
+       CancelClauseSelector
+)
+
+func IsCancelClauseSelector(input *types.TxInput) bool {
+       return len(input.Arguments()) == sizeOfCancelClauseArgs && hex.EncodeToString(input.Arguments()[len(input.Arguments()) - 1]) == hex.EncodeToString(vm.Int64Bytes(CancelClauseSelector))
+}
+
+func IsTradeClauseSelector(input *types.TxInput) bool {
+       return IsPartialTradeClauseSelector(input) || IsFullTradeClauseSelector(input)
+}
+
+func IsPartialTradeClauseSelector(input *types.TxInput) bool {
+       return len(input.Arguments()) == sizeOfPartialTradeClauseArgs && hex.EncodeToString(input.Arguments()[len(input.Arguments()) - 1]) == hex.EncodeToString(vm.Int64Bytes(PartialTradeClauseSelector))
+}
+
+func IsFullTradeClauseSelector(input *types.TxInput) bool {
+       return len(input.Arguments()) == sizeOfFullTradeClauseArgs && hex.EncodeToString(input.Arguments()[len(input.Arguments()) - 1]) == hex.EncodeToString(vm.Int64Bytes(FullTradeClauseSelector))
+}
diff --git a/application/mov/match/match.go b/application/mov/match/match.go
new file mode 100644 (file)
index 0000000..c09ec88
--- /dev/null
@@ -0,0 +1,280 @@
+package match
+
+import (
+       "encoding/hex"
+       "math"
+
+       "github.com/vapor/application/mov/common"
+       "github.com/vapor/application/mov/contract"
+       "github.com/vapor/application/mov/database"
+       "github.com/vapor/consensus/segwit"
+       "github.com/vapor/errors"
+       vprMath "github.com/vapor/math"
+       "github.com/vapor/protocol/bc"
+       "github.com/vapor/protocol/bc/types"
+       "github.com/vapor/protocol/vm"
+       "github.com/vapor/protocol/vm/vmutil"
+)
+
+const maxFeeRate = 0.05
+
+type Engine struct {
+       orderTable  *OrderTable
+       nodeProgram []byte
+}
+
+func NewEngine(movStore database.MovStore, nodeProgram []byte) *Engine {
+       return &Engine{orderTable: NewOrderTable(movStore), nodeProgram: nodeProgram}
+}
+
+func (e *Engine) HasMatchedTx(tradePairs ...*common.TradePair) bool {
+       if err := validateTradePairs(tradePairs); err != nil {
+               return false
+       }
+
+       orders := e.peekOrders(tradePairs)
+       if len(orders) == 0 {
+               return false
+       }
+
+       for i, order := range orders {
+               if canNotBeMatched(order, orders[getOppositeIndex(len(orders), i)]) {
+                       return false
+               }
+       }
+       return true
+}
+
+// NextMatchedTx return the next matchable transaction by the specified trade pairs
+// the size of trade pairs at least 2, and the sequence of trade pairs can form a loop
+// for example, [assetA -> assetB, assetB -> assetC, assetC -> assetA]
+func (e *Engine) NextMatchedTx(tradePairs ...*common.TradePair) (*types.Tx, error) {
+       if err := validateTradePairs(tradePairs); err != nil {
+               return nil, err
+       }
+
+       orders := e.peekOrders(tradePairs)
+       if len(orders) == 0 {
+               return nil, errors.New("no order for the specified trade pair in the order table")
+       }
+
+       tx, err := e.buildMatchTx(orders)
+       if err != nil {
+               return nil, err
+       }
+
+       for _, tradePair := range tradePairs {
+               e.orderTable.PopOrder(tradePair)
+       }
+
+       if err := addPartialTradeOrder(tx, e.orderTable); err != nil {
+               return nil, err
+       }
+       return tx, nil
+}
+
+func (e *Engine) peekOrders(tradePairs []*common.TradePair) []*common.Order {
+       var orders []*common.Order
+       for _, tradePair := range tradePairs {
+               order := e.orderTable.PeekOrder(tradePair)
+               if order == nil {
+                       return nil
+               }
+
+               orders = append(orders, order)
+       }
+       return orders
+}
+
+func validateTradePairs(tradePairs []*common.TradePair) error {
+       if len(tradePairs) < 2 {
+               return errors.New("size of trade pairs at least 2")
+       }
+
+       for i, tradePair := range tradePairs {
+               oppositeTradePair := tradePairs[getOppositeIndex(len(tradePairs), i)]
+               if *tradePair.ToAssetID != *oppositeTradePair.FromAssetID {
+                       return errors.New("specified trade pairs is invalid")
+               }
+       }
+       return nil
+}
+
+func canNotBeMatched(order, oppositeOrder *common.Order) bool {
+       rate := 1 / order.Rate
+       return rate < oppositeOrder.Rate
+}
+
+func (e *Engine) buildMatchTx(orders []*common.Order) (*types.Tx, error) {
+       txData := &types.TxData{Version: 1}
+       for i, order := range orders {
+               input := types.NewSpendInput(nil, *order.Utxo.SourceID, *order.FromAssetID, order.Utxo.Amount, order.Utxo.SourcePos, order.Utxo.ControlProgram)
+               txData.Inputs = append(txData.Inputs, input)
+
+               oppositeOrder := orders[getOppositeIndex(len(orders), i)]
+               if err := addMatchTxOutput(txData, input, order, oppositeOrder.Utxo.Amount); err != nil {
+                       return nil, err
+               }
+       }
+
+       if err := e.addMatchTxFeeOutput(txData); err != nil {
+               return nil, err
+       }
+
+       byteData, err := txData.MarshalText()
+       if err != nil {
+               return nil, err
+       }
+
+       txData.SerializedSize = uint64(len(byteData))
+       return types.NewTx(*txData), nil
+}
+
+func addMatchTxOutput(txData *types.TxData, txInput *types.TxInput, order *common.Order, oppositeAmount uint64) error {
+       contractArgs, err := segwit.DecodeP2WMCProgram(order.Utxo.ControlProgram)
+       if err != nil {
+               return err
+       }
+
+       requestAmount := calcRequestAmount(order.Utxo.Amount, contractArgs)
+       receiveAmount := vprMath.MinUint64(requestAmount, oppositeAmount)
+       shouldPayAmount := CalcShouldPayAmount(receiveAmount, contractArgs)
+       isPartialTrade := order.Utxo.Amount > shouldPayAmount
+
+       setMatchTxArguments(txInput, isPartialTrade, len(txData.Outputs), receiveAmount)
+       txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(*order.ToAssetID, receiveAmount, contractArgs.SellerProgram))
+       if isPartialTrade {
+               txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(*order.FromAssetID, order.Utxo.Amount-shouldPayAmount, order.Utxo.ControlProgram))
+       }
+       return nil
+}
+
+func (e *Engine) addMatchTxFeeOutput(txData *types.TxData) error {
+       feeAssetAmountMap, err := CalcFeeFromMatchedTx(txData)
+       if err != nil {
+               return err
+       }
+
+       for feeAssetID, amount := range feeAssetAmountMap {
+               var reminder int64 = 0
+               feeAmount := amount.payableFeeAmount
+               if amount.payableFeeAmount > amount.maxFeeAmount {
+                       feeAmount = amount.maxFeeAmount
+                       reminder = amount.payableFeeAmount - amount.maxFeeAmount
+               }
+               txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(feeAssetID, uint64(feeAmount), e.nodeProgram))
+
+               // There is the remaining amount after paying the handling fee, assign it evenly to participants in the transaction
+               averageAmount := reminder / int64(len(txData.Inputs))
+               if averageAmount == 0 {
+                       averageAmount = 1
+               }
+               for i := 0; i < len(txData.Inputs) && reminder > 0; i++ {
+                       contractArgs, err := segwit.DecodeP2WMCProgram(txData.Inputs[i].ControlProgram())
+                       if err != nil {
+                               return err
+                       }
+
+                       if i == len(txData.Inputs)-1 {
+                               txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(feeAssetID, uint64(reminder), contractArgs.SellerProgram))
+                       } else {
+                               txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(feeAssetID, uint64(averageAmount), contractArgs.SellerProgram))
+                       }
+                       reminder -= averageAmount
+               }
+       }
+       return nil
+}
+
+func setMatchTxArguments(txInput *types.TxInput, isPartialTrade bool, position int, receiveAmounts uint64) {
+       var arguments [][]byte
+       if isPartialTrade {
+               arguments = [][]byte{vm.Int64Bytes(int64(receiveAmounts)), vm.Int64Bytes(int64(position)), vm.Int64Bytes(contract.PartialTradeClauseSelector)}
+       } else {
+               arguments = [][]byte{vm.Int64Bytes(int64(position)), vm.Int64Bytes(contract.FullTradeClauseSelector)}
+       }
+       txInput.SetArguments(arguments)
+}
+
+func addPartialTradeOrder(tx *types.Tx, orderTable *OrderTable) error {
+       for i, output := range tx.Outputs {
+               if !segwit.IsP2WMCScript(output.ControlProgram()) {
+                       continue
+               }
+
+               order, err := common.NewOrderFromOutput(tx, i)
+               if err != nil {
+                       return err
+               }
+
+               if err := orderTable.AddOrder(order); err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+func getOppositeIndex(size int, selfIdx int) int {
+       oppositeIdx := selfIdx + 1
+       if selfIdx >= size-1 {
+               oppositeIdx = 0
+       }
+       return oppositeIdx
+}
+
+type feeAmount struct {
+       maxFeeAmount     int64
+       payableFeeAmount int64
+}
+
+func CalcFeeFromMatchedTx(txData *types.TxData) (map[bc.AssetID]*feeAmount, error) {
+       assetAmountMap := make(map[bc.AssetID]*feeAmount)
+       for _, input := range txData.Inputs {
+               assetAmountMap[input.AssetID()] = &feeAmount{}
+       }
+
+       receiveOutputMap := make(map[string]*types.TxOutput)
+       for _, output := range txData.Outputs {
+               // minus the amount of the re-order
+               if segwit.IsP2WMCScript(output.ControlProgram()) {
+                       assetAmountMap[*output.AssetAmount().AssetId].payableFeeAmount -= int64(output.AssetAmount().Amount)
+               } else {
+                       receiveOutputMap[hex.EncodeToString(output.ControlProgram())] = output
+               }
+       }
+
+       for _, input := range txData.Inputs {
+               contractArgs, err := segwit.DecodeP2WMCProgram(input.ControlProgram())
+               if err != nil {
+                       return nil, err
+               }
+
+               assetAmountMap[input.AssetID()].payableFeeAmount += int64(input.AssetAmount().Amount)
+               receiveOutput, ok := receiveOutputMap[hex.EncodeToString(contractArgs.SellerProgram)]
+               if !ok {
+                       return nil, errors.New("the input of matched tx has no receive output")
+               }
+
+               assetAmountMap[*receiveOutput.AssetAmount().AssetId].payableFeeAmount -= int64(receiveOutput.AssetAmount().Amount)
+               assetAmountMap[input.AssetID()].maxFeeAmount = CalcMaxFeeAmount(CalcShouldPayAmount(receiveOutput.AssetAmount().Amount, contractArgs))
+       }
+
+       for assetID, amount := range assetAmountMap {
+               if amount.payableFeeAmount == 0 {
+                       delete(assetAmountMap, assetID)
+               }
+       }
+       return assetAmountMap, nil
+}
+
+func calcRequestAmount(fromAmount uint64, contractArg *vmutil.MagneticContractArgs) uint64 {
+       return uint64(int64(fromAmount) * contractArg.RatioNumerator / contractArg.RatioDenominator)
+}
+
+func CalcShouldPayAmount(receiveAmount uint64, contractArg *vmutil.MagneticContractArgs) uint64 {
+       return uint64(math.Ceil(float64(receiveAmount) * float64(contractArg.RatioDenominator) / float64(contractArg.RatioNumerator)))
+}
+
+func CalcMaxFeeAmount(shouldPayAmount uint64) int64 {
+       return int64(math.Ceil(float64(shouldPayAmount) * maxFeeRate))
+}
diff --git a/application/mov/match/match_test.go b/application/mov/match/match_test.go
new file mode 100644 (file)
index 0000000..8101824
--- /dev/null
@@ -0,0 +1,230 @@
+package match
+
+import (
+       "testing"
+
+       "github.com/vapor/protocol/vm"
+       "github.com/vapor/testutil"
+
+       "github.com/vapor/application/mov/common"
+       "github.com/vapor/application/mov/database"
+       "github.com/vapor/protocol/bc"
+       "github.com/vapor/protocol/bc/types"
+       "github.com/vapor/protocol/vm/vmutil"
+)
+
+var (
+       btc = bc.NewAssetID([32]byte{1})
+       eth = bc.NewAssetID([32]byte{2})
+
+       orders = []*common.Order{
+               // btc -> eth
+               {
+                       FromAssetID: &btc,
+                       ToAssetID:   &eth,
+                       Rate:        50,
+                       Utxo: &common.MovUtxo{
+                               SourceID:       hashPtr(testutil.MustDecodeHash("37b8edf656e45a7addf47f5626e114a8c394d918a36f61b5a2905675a09b40ae")),
+                               SourcePos:      0,
+                               Amount:         10,
+                               ControlProgram: mustCreateP2WMCProgram(eth, testutil.MustDecodeHexString("51"), 50, 1),
+                       },
+               },
+               {
+                       FromAssetID: &btc,
+                       ToAssetID:   &eth,
+                       Rate:        53,
+                       Utxo: &common.MovUtxo{
+                               SourceID:       hashPtr(testutil.MustDecodeHash("3ec2bbfb499a8736d377b547eee5392bcddf7ec2b287e9ed20b5938c3d84e7cd")),
+                               SourcePos:      0,
+                               Amount:         20,
+                               ControlProgram: mustCreateP2WMCProgram(eth, testutil.MustDecodeHexString("52"), 53, 1),
+                       },
+               },
+
+               // eth -> btc
+               {
+                       FromAssetID: &eth,
+                       ToAssetID:   &btc,
+                       Rate:        1 / 51.0,
+                       Utxo: &common.MovUtxo{
+                               SourceID:       hashPtr(testutil.MustDecodeHash("fba43ff5155209cb1769e2ec0e1d4a33accf899c740865edfc6d1de39b873b29")),
+                               SourcePos:      0,
+                               Amount:         510,
+                               ControlProgram: mustCreateP2WMCProgram(btc, testutil.MustDecodeHexString("53"), 1, 51.0),
+                       },
+               },
+               {
+                       FromAssetID: &eth,
+                       ToAssetID:   &btc,
+                       Rate:        1 / 52.0,
+                       Utxo: &common.MovUtxo{
+                               SourceID:       hashPtr(testutil.MustDecodeHash("05f24bb847db823075d81786aa270748e02602199cd009c0284f928503846a5a")),
+                               SourcePos:      0,
+                               Amount:         416,
+                               ControlProgram: mustCreateP2WMCProgram(btc, testutil.MustDecodeHexString("54"), 1, 52.0),
+                       },
+               },
+               {
+                       FromAssetID: &eth,
+                       ToAssetID:   &btc,
+                       Rate:        1 / 54.0,
+                       Utxo: &common.MovUtxo{
+                               SourceID:       hashPtr(testutil.MustDecodeHash("119a02980796dc352cf6475457463aef5666f66622088de3551fa73a65f0d201")),
+                               SourcePos:      0,
+                               Amount:         810,
+                               ControlProgram: mustCreateP2WMCProgram(btc, testutil.MustDecodeHexString("55"), 1, 54.0),
+                       },
+               },
+       }
+)
+
+func TestGenerateMatchedTxs(t *testing.T) {
+       btc2eth := &common.TradePair{FromAssetID: &btc, ToAssetID: &eth}
+       eth2btc := &common.TradePair{FromAssetID: &eth, ToAssetID: &btc}
+
+       cases := []struct {
+               desc           string
+               tradePair      *common.TradePair
+               storeOrderMap  map[string][]*common.Order
+               wantMatchedTxs []*types.TxData
+       }{
+               {
+                       desc:      "full matched",
+                       tradePair: &common.TradePair{FromAssetID: &btc, ToAssetID: &eth},
+                       storeOrderMap: map[string][]*common.Order{
+                               btc2eth.Key(): {orders[0], orders[1]},
+                               eth2btc.Key(): {orders[2], orders[3]},
+                       },
+                       wantMatchedTxs: []*types.TxData{
+                               {
+                                       Inputs: []*types.TxInput{
+                                               types.NewSpendInput([][]byte{vm.Int64Bytes(0), vm.Int64Bytes(1)}, *orders[0].Utxo.SourceID, *orders[0].FromAssetID, orders[0].Utxo.Amount, orders[0].Utxo.SourcePos, orders[0].Utxo.ControlProgram),
+                                               types.NewSpendInput([][]byte{vm.Int64Bytes(1), vm.Int64Bytes(1)}, *orders[2].Utxo.SourceID, *orders[2].FromAssetID, orders[2].Utxo.Amount, orders[2].Utxo.SourcePos, orders[2].Utxo.ControlProgram),
+                                       },
+                                       Outputs: []*types.TxOutput{
+                                               types.NewIntraChainOutput(*orders[0].ToAssetID, 500, testutil.MustDecodeHexString("51")),
+                                               types.NewIntraChainOutput(*orders[2].ToAssetID, 10, testutil.MustDecodeHexString("53")),
+                                               types.NewIntraChainOutput(*orders[0].ToAssetID, 10, []byte{0x51}),
+                                       },
+                               },
+                       },
+               },
+               {
+                       desc:      "partial matched",
+                       tradePair: &common.TradePair{FromAssetID: &btc, ToAssetID: &eth},
+                       storeOrderMap: map[string][]*common.Order{
+                               btc2eth.Key(): {orders[0], orders[1]},
+                               eth2btc.Key(): {orders[3]},
+                       },
+                       wantMatchedTxs: []*types.TxData{
+                               {
+                                       Inputs: []*types.TxInput{
+                                               types.NewSpendInput([][]byte{vm.Int64Bytes(416), vm.Int64Bytes(0), vm.Int64Bytes(0)}, *orders[0].Utxo.SourceID, *orders[0].FromAssetID, orders[0].Utxo.Amount, orders[0].Utxo.SourcePos, orders[0].Utxo.ControlProgram),
+                                               types.NewSpendInput([][]byte{vm.Int64Bytes(2), vm.Int64Bytes(1)}, *orders[3].Utxo.SourceID, *orders[3].FromAssetID, orders[3].Utxo.Amount, orders[3].Utxo.SourcePos, orders[3].Utxo.ControlProgram),
+                                       },
+                                       Outputs: []*types.TxOutput{
+                                               types.NewIntraChainOutput(*orders[0].ToAssetID, 416, testutil.MustDecodeHexString("51")),
+                                               // re-order
+                                               types.NewIntraChainOutput(*orders[0].FromAssetID, 1, orders[0].Utxo.ControlProgram),
+                                               types.NewIntraChainOutput(*orders[3].ToAssetID, 8, testutil.MustDecodeHexString("54")),
+                                               // fee
+                                               types.NewIntraChainOutput(*orders[3].ToAssetID, 1, []byte{0x51}),
+                                       },
+                               },
+                       },
+               },
+               {
+                       desc:      "partial matched and continue to match",
+                       tradePair: &common.TradePair{FromAssetID: &btc, ToAssetID: &eth},
+                       storeOrderMap: map[string][]*common.Order{
+                               btc2eth.Key(): {orders[0], orders[1]},
+                               eth2btc.Key(): {orders[4]},
+                       },
+                       wantMatchedTxs: []*types.TxData{
+                               {
+                                       Inputs: []*types.TxInput{
+                                               types.NewSpendInput([][]byte{vm.Int64Bytes(0), vm.Int64Bytes(1)}, *orders[0].Utxo.SourceID, *orders[0].FromAssetID, orders[0].Utxo.Amount, orders[0].Utxo.SourcePos, orders[0].Utxo.ControlProgram),
+                                               types.NewSpendInput([][]byte{vm.Int64Bytes(10), vm.Int64Bytes(1), vm.Int64Bytes(0)}, *orders[4].Utxo.SourceID, *orders[4].FromAssetID, orders[4].Utxo.Amount, orders[4].Utxo.SourcePos, orders[4].Utxo.ControlProgram),
+                                       },
+                                       Outputs: []*types.TxOutput{
+                                               types.NewIntraChainOutput(*orders[0].ToAssetID, 500, testutil.MustDecodeHexString("51")),
+                                               types.NewIntraChainOutput(*orders[4].ToAssetID, 10, testutil.MustDecodeHexString("55")),
+                                               // re-order
+                                               types.NewIntraChainOutput(*orders[4].FromAssetID, 270, orders[4].Utxo.ControlProgram),
+                                               // fee
+                                               types.NewIntraChainOutput(*orders[4].FromAssetID, 27, []byte{0x51}),
+                                               // refund
+                                               types.NewIntraChainOutput(*orders[4].FromAssetID, 6, testutil.MustDecodeHexString("51")),
+                                               types.NewIntraChainOutput(*orders[4].FromAssetID, 7, testutil.MustDecodeHexString("55")),
+                                       },
+                               },
+                               {
+                                       Inputs: []*types.TxInput{
+                                               types.NewSpendInput([][]byte{vm.Int64Bytes(0), vm.Int64Bytes(0)}, *orders[1].Utxo.SourceID, *orders[1].FromAssetID, orders[1].Utxo.Amount, orders[1].Utxo.SourcePos, orders[1].Utxo.ControlProgram),
+                                               types.NewSpendInput([][]byte{vm.Int64Bytes(2), vm.Int64Bytes(1)}, testutil.MustDecodeHash("f47177c12d25f5316eb377ea006e77bf07e4f9646860e4641e313e004f9aa989"), *orders[4].FromAssetID, 270, 2, orders[4].Utxo.ControlProgram),
+                                       },
+                                       Outputs: []*types.TxOutput{
+                                               types.NewIntraChainOutput(*orders[1].ToAssetID, 270, testutil.MustDecodeHexString("52")),
+                                               // re-order
+                                               types.NewIntraChainOutput(*orders[1].FromAssetID, 14, orders[1].Utxo.ControlProgram),
+                                               types.NewIntraChainOutput(*orders[4].ToAssetID, 5, testutil.MustDecodeHexString("55")),
+                                               types.NewIntraChainOutput(*orders[1].FromAssetID, 1, []byte{0x51}),
+                                       },
+                               },
+                       },
+               },
+       }
+
+       for i, c := range cases {
+               movStore := &database.MockMovStore{OrderMap: c.storeOrderMap}
+               matchEngine := NewEngine(movStore, []byte{0x51})
+               var gotMatchedTxs []*types.Tx
+               for matchEngine.HasMatchedTx(c.tradePair, c.tradePair.Reverse()) {
+                       matchedTx, err := matchEngine.NextMatchedTx(c.tradePair, c.tradePair.Reverse())
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+
+                       gotMatchedTxs = append(gotMatchedTxs, matchedTx)
+               }
+
+               if len(c.wantMatchedTxs) != len(gotMatchedTxs) {
+                       t.Errorf("#%d(%s) the length of got matched tx is not equals want matched tx", i, c.desc)
+                       continue
+               }
+
+               for i, gotMatchedTx := range gotMatchedTxs {
+                       c.wantMatchedTxs[i].Version = 1
+                       byteData, err := c.wantMatchedTxs[i].MarshalText()
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+
+                       c.wantMatchedTxs[i].SerializedSize = uint64(len(byteData))
+                       wantMatchedTx := types.NewTx(*c.wantMatchedTxs[i])
+                       if gotMatchedTx.ID != wantMatchedTx.ID {
+                               t.Errorf("#%d(%s) the tx hash of got matched tx: %s is not equals want matched tx: %s", i, c.desc, gotMatchedTx.ID.String(), wantMatchedTx.ID.String())
+                       }
+               }
+       }
+}
+
+func mustCreateP2WMCProgram(requestAsset bc.AssetID, sellerProgram []byte, ratioMolecule, ratioDenominator int64) []byte {
+       contractArgs := vmutil.MagneticContractArgs{
+               RequestedAsset:   requestAsset,
+               RatioNumerator:   ratioMolecule,
+               RatioDenominator: ratioDenominator,
+               SellerProgram:    sellerProgram,
+               SellerKey:        testutil.MustDecodeHexString("ad79ec6bd3a6d6dbe4d0ee902afc99a12b9702fb63edce5f651db3081d868b75"),
+       }
+       program, err := vmutil.P2WMCProgram(contractArgs)
+       if err != nil {
+               panic(err)
+       }
+       return program
+}
+
+func hashPtr(hash bc.Hash) *bc.Hash {
+       return &hash
+}
diff --git a/math/algorithm.go b/math/algorithm.go
new file mode 100644 (file)
index 0000000..a596496
--- /dev/null
@@ -0,0 +1,8 @@
+package math
+
+func MinUint64(x, y uint64) uint64 {
+       if x < y {
+               return x
+       }
+       return y
+}