7 "github.com/vapor/application/mov/common"
8 "github.com/vapor/application/mov/contract"
9 "github.com/vapor/application/mov/database"
10 "github.com/vapor/consensus/segwit"
11 "github.com/vapor/errors"
12 vprMath "github.com/vapor/math"
13 "github.com/vapor/protocol/bc"
14 "github.com/vapor/protocol/bc/types"
15 "github.com/vapor/protocol/vm"
16 "github.com/vapor/protocol/vm/vmutil"
19 const maxFeeRate = 0.05
22 orderTable *OrderTable
26 func NewEngine(movStore database.MovStore, nodeProgram []byte) *Engine {
27 return &Engine{orderTable: NewOrderTable(movStore), nodeProgram: nodeProgram}
30 func (e *Engine) HasMatchedTx(tradePairs ...*common.TradePair) bool {
31 if err := validateTradePairs(tradePairs); err != nil {
35 orders := e.peekOrders(tradePairs)
40 for i, order := range orders {
41 if canNotBeMatched(order, orders[getOppositeIndex(len(orders), i)]) {
48 // NextMatchedTx return the next matchable transaction by the specified trade pairs
49 // the size of trade pairs at least 2, and the sequence of trade pairs can form a loop
50 // for example, [assetA -> assetB, assetB -> assetC, assetC -> assetA]
51 func (e *Engine) NextMatchedTx(tradePairs ...*common.TradePair) (*types.Tx, error) {
52 if err := validateTradePairs(tradePairs); err != nil {
56 orders := e.peekOrders(tradePairs)
58 return nil, errors.New("no order for the specified trade pair in the order table")
61 tx, err := e.buildMatchTx(orders)
66 for _, tradePair := range tradePairs {
67 e.orderTable.PopOrder(tradePair)
70 if err := addPartialTradeOrder(tx, e.orderTable); err != nil {
76 func (e *Engine) peekOrders(tradePairs []*common.TradePair) []*common.Order {
77 var orders []*common.Order
78 for _, tradePair := range tradePairs {
79 order := e.orderTable.PeekOrder(tradePair)
84 orders = append(orders, order)
89 func validateTradePairs(tradePairs []*common.TradePair) error {
90 if len(tradePairs) < 2 {
91 return errors.New("size of trade pairs at least 2")
94 for i, tradePair := range tradePairs {
95 oppositeTradePair := tradePairs[getOppositeIndex(len(tradePairs), i)]
96 if *tradePair.ToAssetID != *oppositeTradePair.FromAssetID {
97 return errors.New("specified trade pairs is invalid")
103 func canNotBeMatched(order, oppositeOrder *common.Order) bool {
104 rate := 1 / order.Rate
105 return rate < oppositeOrder.Rate
108 func (e *Engine) buildMatchTx(orders []*common.Order) (*types.Tx, error) {
109 txData := &types.TxData{Version: 1}
110 for i, order := range orders {
111 input := types.NewSpendInput(nil, *order.Utxo.SourceID, *order.FromAssetID, order.Utxo.Amount, order.Utxo.SourcePos, order.Utxo.ControlProgram)
112 txData.Inputs = append(txData.Inputs, input)
114 oppositeOrder := orders[getOppositeIndex(len(orders), i)]
115 if err := addMatchTxOutput(txData, input, order, oppositeOrder.Utxo.Amount); err != nil {
120 if err := e.addMatchTxFeeOutput(txData); err != nil {
124 byteData, err := txData.MarshalText()
129 txData.SerializedSize = uint64(len(byteData))
130 return types.NewTx(*txData), nil
133 func addMatchTxOutput(txData *types.TxData, txInput *types.TxInput, order *common.Order, oppositeAmount uint64) error {
134 contractArgs, err := segwit.DecodeP2WMCProgram(order.Utxo.ControlProgram)
139 requestAmount := calcRequestAmount(order.Utxo.Amount, contractArgs)
140 receiveAmount := vprMath.MinUint64(requestAmount, oppositeAmount)
141 shouldPayAmount := CalcShouldPayAmount(receiveAmount, contractArgs)
142 isPartialTrade := order.Utxo.Amount > shouldPayAmount
144 setMatchTxArguments(txInput, isPartialTrade, len(txData.Outputs), receiveAmount)
145 txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(*order.ToAssetID, receiveAmount, contractArgs.SellerProgram))
147 txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(*order.FromAssetID, order.Utxo.Amount-shouldPayAmount, order.Utxo.ControlProgram))
152 func (e *Engine) addMatchTxFeeOutput(txData *types.TxData) error {
153 feeAssetAmountMap, err := CalcFeeFromMatchedTx(txData)
158 for feeAssetID, amount := range feeAssetAmountMap {
159 var reminder int64 = 0
160 feeAmount := amount.payableFeeAmount
161 if amount.payableFeeAmount > amount.maxFeeAmount {
162 feeAmount = amount.maxFeeAmount
163 reminder = amount.payableFeeAmount - amount.maxFeeAmount
165 txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(feeAssetID, uint64(feeAmount), e.nodeProgram))
167 // There is the remaining amount after paying the handling fee, assign it evenly to participants in the transaction
168 averageAmount := reminder / int64(len(txData.Inputs))
169 if averageAmount == 0 {
172 for i := 0; i < len(txData.Inputs) && reminder > 0; i++ {
173 contractArgs, err := segwit.DecodeP2WMCProgram(txData.Inputs[i].ControlProgram())
178 if i == len(txData.Inputs)-1 {
179 txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(feeAssetID, uint64(reminder), contractArgs.SellerProgram))
181 txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(feeAssetID, uint64(averageAmount), contractArgs.SellerProgram))
183 reminder -= averageAmount
189 func setMatchTxArguments(txInput *types.TxInput, isPartialTrade bool, position int, receiveAmounts uint64) {
190 var arguments [][]byte
192 arguments = [][]byte{vm.Int64Bytes(int64(receiveAmounts)), vm.Int64Bytes(int64(position)), vm.Int64Bytes(contract.PartialTradeClauseSelector)}
194 arguments = [][]byte{vm.Int64Bytes(int64(position)), vm.Int64Bytes(contract.FullTradeClauseSelector)}
196 txInput.SetArguments(arguments)
199 func addPartialTradeOrder(tx *types.Tx, orderTable *OrderTable) error {
200 for i, output := range tx.Outputs {
201 if !segwit.IsP2WMCScript(output.ControlProgram()) {
205 order, err := common.NewOrderFromOutput(tx, i)
210 if err := orderTable.AddOrder(order); err != nil {
217 func getOppositeIndex(size int, selfIdx int) int {
218 oppositeIdx := selfIdx + 1
219 if selfIdx >= size-1 {
225 type feeAmount struct {
227 payableFeeAmount int64
230 func CalcFeeFromMatchedTx(txData *types.TxData) (map[bc.AssetID]*feeAmount, error) {
231 assetAmountMap := make(map[bc.AssetID]*feeAmount)
232 for _, input := range txData.Inputs {
233 assetAmountMap[input.AssetID()] = &feeAmount{}
236 receiveOutputMap := make(map[string]*types.TxOutput)
237 for _, output := range txData.Outputs {
238 // minus the amount of the re-order
239 if segwit.IsP2WMCScript(output.ControlProgram()) {
240 assetAmountMap[*output.AssetAmount().AssetId].payableFeeAmount -= int64(output.AssetAmount().Amount)
242 receiveOutputMap[hex.EncodeToString(output.ControlProgram())] = output
246 for _, input := range txData.Inputs {
247 contractArgs, err := segwit.DecodeP2WMCProgram(input.ControlProgram())
252 assetAmountMap[input.AssetID()].payableFeeAmount += int64(input.AssetAmount().Amount)
253 receiveOutput, ok := receiveOutputMap[hex.EncodeToString(contractArgs.SellerProgram)]
255 return nil, errors.New("the input of matched tx has no receive output")
258 assetAmountMap[*receiveOutput.AssetAmount().AssetId].payableFeeAmount -= int64(receiveOutput.AssetAmount().Amount)
259 assetAmountMap[input.AssetID()].maxFeeAmount = CalcMaxFeeAmount(CalcShouldPayAmount(receiveOutput.AssetAmount().Amount, contractArgs))
262 for assetID, amount := range assetAmountMap {
263 if amount.payableFeeAmount == 0 {
264 delete(assetAmountMap, assetID)
267 return assetAmountMap, nil
270 func calcRequestAmount(fromAmount uint64, contractArg *vmutil.MagneticContractArgs) uint64 {
271 return uint64(int64(fromAmount) * contractArg.RatioNumerator / contractArg.RatioDenominator)
274 func CalcShouldPayAmount(receiveAmount uint64, contractArg *vmutil.MagneticContractArgs) uint64 {
275 return uint64(math.Ceil(float64(receiveAmount) * float64(contractArg.RatioDenominator) / float64(contractArg.RatioNumerator)))
278 func CalcMaxFeeAmount(shouldPayAmount uint64) int64 {
279 return int64(math.Ceil(float64(shouldPayAmount) * maxFeeRate))