8 "github.com/vapor/application/mov/common"
9 "github.com/vapor/application/mov/contract"
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 // Engine is used to generate math transactions
21 orderTable *OrderTable
26 // NewEngine return a new Engine
27 func NewEngine(orderTable *OrderTable, maxFeeRate float64, nodeProgram []byte) *Engine {
28 return &Engine{orderTable: orderTable, maxFeeRate: maxFeeRate, nodeProgram: nodeProgram}
31 // HasMatchedTx check does the input trade pair can generate a match deal
32 func (e *Engine) HasMatchedTx(tradePairs ...*common.TradePair) bool {
33 if err := validateTradePairs(tradePairs); err != nil {
37 orders := e.peekOrders(tradePairs)
42 return isMatched(orders)
45 // NextMatchedTx return the next matchable transaction by the specified trade pairs
46 // the size of trade pairs at least 2, and the sequence of trade pairs can form a loop
47 // for example, [assetA -> assetB, assetB -> assetC, assetC -> assetA]
48 func (e *Engine) NextMatchedTx(tradePairs ...*common.TradePair) (*types.Tx, error) {
49 if !e.HasMatchedTx(tradePairs...) {
50 return nil, errors.New("the specified trade pairs can not be matched")
53 tx, err := e.buildMatchTx(e.peekOrders(tradePairs))
58 for _, tradePair := range tradePairs {
59 e.orderTable.PopOrder(tradePair)
62 if err := e.addPartialTradeOrder(tx); err != nil {
68 func (e *Engine) addMatchTxFeeOutput(txData *types.TxData) error {
69 txFee, err := CalcMatchedTxFee(txData, e.maxFeeRate)
74 for assetID, matchTxFee := range txFee {
75 feeAmount, reminder := matchTxFee.FeeAmount, int64(0)
76 if matchTxFee.FeeAmount > matchTxFee.MaxFeeAmount {
77 feeAmount = matchTxFee.MaxFeeAmount
78 reminder = matchTxFee.FeeAmount - matchTxFee.MaxFeeAmount
80 txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(assetID, uint64(feeAmount), e.nodeProgram))
82 // There is the remaining amount after paying the handling fee, assign it evenly to participants in the transaction
83 averageAmount := reminder / int64(len(txData.Inputs))
84 if averageAmount == 0 {
88 for i := 0; i < len(txData.Inputs) && reminder > 0; i++ {
89 contractArgs, err := segwit.DecodeP2WMCProgram(txData.Inputs[i].ControlProgram())
94 if i == len(txData.Inputs)-1 {
95 txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(assetID, uint64(reminder), contractArgs.SellerProgram))
97 txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(assetID, uint64(averageAmount), contractArgs.SellerProgram))
99 reminder -= averageAmount
105 func (e *Engine) addPartialTradeOrder(tx *types.Tx) error {
106 for i, output := range tx.Outputs {
107 if !segwit.IsP2WMCScript(output.ControlProgram()) {
111 order, err := common.NewOrderFromOutput(tx, i)
116 if err := e.orderTable.AddOrder(order); err != nil {
123 func (e *Engine) buildMatchTx(orders []*common.Order) (*types.Tx, error) {
124 txData := &types.TxData{Version: 1}
125 for i, order := range orders {
126 input := types.NewSpendInput(nil, *order.Utxo.SourceID, *order.FromAssetID, order.Utxo.Amount, order.Utxo.SourcePos, order.Utxo.ControlProgram)
127 txData.Inputs = append(txData.Inputs, input)
129 oppositeOrder := orders[calcOppositeIndex(len(orders), i)]
130 if err := addMatchTxOutput(txData, input, order, oppositeOrder.Utxo.Amount); err != nil {
135 if err := e.addMatchTxFeeOutput(txData); err != nil {
139 byteData, err := txData.MarshalText()
144 txData.SerializedSize = uint64(len(byteData))
145 return types.NewTx(*txData), nil
148 func (e *Engine) peekOrders(tradePairs []*common.TradePair) []*common.Order {
149 var orders []*common.Order
150 for _, tradePair := range tradePairs {
151 order := e.orderTable.PeekOrder(tradePair)
156 orders = append(orders, order)
161 // MatchedTxFee is object to record the mov tx's fee information
162 type MatchedTxFee struct {
167 // CalcMatchedTxFee is used to calculate tx's MatchedTxFees
168 func CalcMatchedTxFee(txData *types.TxData, maxFeeRate float64) (map[bc.AssetID]*MatchedTxFee, error) {
169 assetFeeMap := make(map[bc.AssetID]*MatchedTxFee)
170 dealProgMaps := make(map[string]bool)
172 for _, input := range txData.Inputs {
173 assetFeeMap[input.AssetID()] = &MatchedTxFee{FeeAmount: int64(input.AssetAmount().Amount)}
174 contractArgs, err := segwit.DecodeP2WMCProgram(input.ControlProgram())
179 dealProgMaps[hex.EncodeToString(contractArgs.SellerProgram)] = true
182 for _, input := range txData.Inputs {
183 contractArgs, err := segwit.DecodeP2WMCProgram(input.ControlProgram())
188 oppositeAmount := uint64(assetFeeMap[contractArgs.RequestedAsset].FeeAmount)
189 receiveAmount := vprMath.MinUint64(CalcRequestAmount(input.Amount(), contractArgs), oppositeAmount)
190 assetFeeMap[input.AssetID()].MaxFeeAmount = calcMaxFeeAmount(calcShouldPayAmount(receiveAmount, contractArgs), maxFeeRate)
193 for _, output := range txData.Outputs {
194 assetAmount := output.AssetAmount()
195 if _, ok := dealProgMaps[hex.EncodeToString(output.ControlProgram())]; ok || segwit.IsP2WMCScript(output.ControlProgram()) {
196 assetFeeMap[*assetAmount.AssetId].FeeAmount -= int64(assetAmount.Amount)
197 if assetFeeMap[*assetAmount.AssetId].FeeAmount <= 0 {
198 delete(assetFeeMap, *assetAmount.AssetId)
202 return assetFeeMap, nil
205 func addMatchTxOutput(txData *types.TxData, txInput *types.TxInput, order *common.Order, oppositeAmount uint64) error {
206 contractArgs, err := segwit.DecodeP2WMCProgram(order.Utxo.ControlProgram)
211 requestAmount := CalcRequestAmount(order.Utxo.Amount, contractArgs)
212 receiveAmount := vprMath.MinUint64(requestAmount, oppositeAmount)
213 shouldPayAmount := calcShouldPayAmount(receiveAmount, contractArgs)
214 isPartialTrade := requestAmount > receiveAmount
216 setMatchTxArguments(txInput, isPartialTrade, len(txData.Outputs), receiveAmount)
217 txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(*order.ToAssetID, receiveAmount, contractArgs.SellerProgram))
219 txData.Outputs = append(txData.Outputs, types.NewIntraChainOutput(*order.FromAssetID, order.Utxo.Amount-shouldPayAmount, order.Utxo.ControlProgram))
224 func CalcRequestAmount(fromAmount uint64, contractArg *vmutil.MagneticContractArgs) uint64 {
225 res := big.NewInt(0).SetUint64(fromAmount)
226 res.Mul(res, big.NewInt(contractArg.RatioNumerator)).Quo(res, big.NewInt(contractArg.RatioDenominator))
233 func calcShouldPayAmount(receiveAmount uint64, contractArg *vmutil.MagneticContractArgs) uint64 {
234 res := big.NewInt(0).SetUint64(receiveAmount)
235 res.Mul(res, big.NewInt(contractArg.RatioDenominator)).Quo(res, big.NewInt(contractArg.RatioNumerator))
242 func calcMaxFeeAmount(shouldPayAmount uint64, maxFeeRate float64) int64 {
243 return int64(math.Ceil(float64(shouldPayAmount) * maxFeeRate))
246 func calcOppositeIndex(size int, selfIdx int) int {
247 return (selfIdx + 1) % size
250 func isMatched(orders []*common.Order) bool {
251 for i, order := range orders {
252 if oppositeOrder := orders[calcOppositeIndex(len(orders), i)]; 1/order.Rate < oppositeOrder.Rate {
259 func setMatchTxArguments(txInput *types.TxInput, isPartialTrade bool, position int, receiveAmounts uint64) {
260 var arguments [][]byte
262 arguments = [][]byte{vm.Int64Bytes(int64(receiveAmounts)), vm.Int64Bytes(int64(position)), vm.Int64Bytes(contract.PartialTradeClauseSelector)}
264 arguments = [][]byte{vm.Int64Bytes(int64(position)), vm.Int64Bytes(contract.FullTradeClauseSelector)}
266 txInput.SetArguments(arguments)
269 func validateTradePairs(tradePairs []*common.TradePair) error {
270 if len(tradePairs) < 2 {
271 return errors.New("size of trade pairs at least 2")
274 for i, tradePair := range tradePairs {
275 oppositeTradePair := tradePairs[calcOppositeIndex(len(tradePairs), i)]
276 if *tradePair.ToAssetID != *oppositeTradePair.FromAssetID {
277 return errors.New("specified trade pairs is invalid")