diff --git a/client/mm/exchange_adaptor.go b/client/mm/exchange_adaptor.go index 0c69412283..8ec5820098 100644 --- a/client/mm/exchange_adaptor.go +++ b/client/mm/exchange_adaptor.go @@ -255,10 +255,13 @@ type pendingDEXOrder struct { // swaps, redeems, and refunds are caches of transactions. This avoids // having to query the wallet for transactions that are already confirmed. - txsMtx sync.RWMutex - swaps map[string]*asset.WalletTransaction - redeems map[string]*asset.WalletTransaction - refunds map[string]*asset.WalletTransaction + txsMtx sync.RWMutex + swaps map[string]*asset.WalletTransaction + swapCoinIDToTxID map[string]string + redeems map[string]*asset.WalletTransaction + redeemCoinIDToTxID map[string]string + refunds map[string]*asset.WalletTransaction + refundCoinIDToTxID map[string]string // txsMtx is required to be locked for writes to state state atomic.Value // *dexOrderState @@ -699,8 +702,13 @@ type dexOrderInfo struct { func (u *unifiedExchangeAdaptor) updateDEXOrderEvent(o *pendingDEXOrder, complete bool) { o.txsMtx.RLock() transactions := make([]*asset.WalletTransaction, 0, len(o.swaps)+len(o.redeems)+len(o.refunds)) + txIDSeen := make(map[string]bool) addTxs := func(txs map[string]*asset.WalletTransaction) { for _, tx := range txs { + if txIDSeen[tx.ID] { + continue + } + txIDSeen[tx.ID] = true transactions = append(transactions, tx) } } @@ -994,13 +1002,16 @@ func (u *unifiedExchangeAdaptor) placeMultiTrade(placements []*dexOrderInfo, sel cexEffects.Reserved[toAsset] = reserved pendingOrder := &pendingDEXOrder{ - eventLogID: u.eventLogID.Add(1), - timestamp: time.Now().Unix(), - swaps: make(map[string]*asset.WalletTransaction), - redeems: make(map[string]*asset.WalletTransaction), - refunds: make(map[string]*asset.WalletTransaction), - placementIndex: placements[i].placementIndex, - counterTradeRate: placements[i].counterTradeRate, + eventLogID: u.eventLogID.Add(1), + timestamp: time.Now().Unix(), + swaps: make(map[string]*asset.WalletTransaction), + redeems: make(map[string]*asset.WalletTransaction), + refunds: make(map[string]*asset.WalletTransaction), + swapCoinIDToTxID: make(map[string]string), + redeemCoinIDToTxID: make(map[string]string), + refundCoinIDToTxID: make(map[string]string), + placementIndex: placements[i].placementIndex, + counterTradeRate: placements[i].counterTradeRate, } pendingOrder.state.Store( @@ -2532,9 +2543,10 @@ func dexOrderComplete(o *core.Order) bool { return o.AllFeesConfirmed } -// orderTransactions returns all of the swap, redeem, and refund transactions -// involving a dex order. -func orderTransactions(o *core.Order) (swaps map[string]bool, redeems map[string]bool, refunds map[string]bool) { +// orderCoinIDs returns all of the swap, redeem, and refund transactions +// involving a dex order. There may be multiple coin IDs representing the +// same transaction. +func orderCoinIDs(o *core.Order) (swaps map[string]bool, redeems map[string]bool, refunds map[string]bool) { swaps = make(map[string]bool) redeems = make(map[string]bool) refunds = make(map[string]bool) @@ -2652,34 +2664,44 @@ func dexOrderEffects(o *core.Order, swaps, redeems, refunds map[string]*asset.Wa // The mutex only needs to be locked for reading if the caller wants a consistent // view of the transactions and the state. func (p *pendingDEXOrder) updateState(o *core.Order, getTx func(uint32, string) (*asset.WalletTransaction, error), baseTraits, quoteTraits asset.WalletTrait) { - swaps, redeems, refunds := orderTransactions(o) + swaps, redeems, refunds := orderCoinIDs(o) + // Add new txs to tx cache fromAsset, _, toAsset, _ := orderAssets(o.BaseID, o.QuoteID, o.Sell) - processTxs := func(assetID uint32, m map[string]*asset.WalletTransaction, txs map[string]bool) { - // Add new txs to tx cache - for txID := range txs { - if _, found := m[txID]; !found { - m[txID] = &asset.WalletTransaction{} - } - } - + processTxs := func(assetID uint32, txs map[string]*asset.WalletTransaction, coinIDs map[string]bool, coinIDToTxID map[string]string) { // Query the wallet regarding all unconfirmed transactions - for txID, oldTx := range m { + for txID, oldTx := range txs { if oldTx.Confirmed { continue } tx, err := getTx(assetID, txID) if err != nil { - // p.log.Errorf("Error getting tx %s: %v", txID, err) continue } - m[txID] = tx + txs[tx.ID] = tx + } + + // Add new txs to tx cache + for coinID := range coinIDs { + txID, found := coinIDToTxID[coinID] + if found { + continue + } + if _, found := txs[txID]; found { + continue + } + tx, err := getTx(assetID, coinID) + if err != nil { + continue + } + coinIDToTxID[coinID] = tx.ID + txs[tx.ID] = tx } } - processTxs(fromAsset, p.swaps, swaps) - processTxs(toAsset, p.redeems, redeems) - processTxs(fromAsset, p.refunds, refunds) + processTxs(fromAsset, p.swaps, swaps, p.swapCoinIDToTxID) + processTxs(toAsset, p.redeems, redeems, p.redeemCoinIDToTxID) + processTxs(fromAsset, p.refunds, refunds, p.refundCoinIDToTxID) dexEffects, cexEffects := dexOrderEffects(o, p.swaps, p.redeems, p.refunds, p.counterTradeRate, baseTraits, quoteTraits) p.state.Store(&dexOrderState{ diff --git a/client/mm/exchange_adaptor_test.go b/client/mm/exchange_adaptor_test.go index c568c4541c..3d8231f4a1 100644 --- a/client/mm/exchange_adaptor_test.go +++ b/client/mm/exchange_adaptor_test.go @@ -2881,7 +2881,7 @@ func TestDEXTrade(t *testing.T) { type updatesAndBalances struct { orderUpdate *orderUpdate - txUpdates []*asset.WalletTransaction + txUpdates map[string]*asset.WalletTransaction stats *RunStats numPendingTrades int } @@ -2909,6 +2909,13 @@ func TestDEXTrade(t *testing.T) { quoteLot1, quoteLot2 := b2q(rate1, lotSize), b2q(rate2, lotSize) quotePerLot1, quotePerLot2 := quoteLot1+buyFees, quoteLot2+buyFees + // This emulates the coinIDs of UTXO coins, which have the + // vout appended to the tx id. + suffixedCoinID := func(id string, suffix int) *string { + s := fmt.Sprintf("%s0%d", id, suffix) + return &s + } + tests := []*test{ { name: "non dynamic swapper, sell", @@ -2934,8 +2941,8 @@ func TestDEXTrade(t *testing.T) { updatesAndBalances: []*updatesAndBalances{ // First order has a match and sends a swap tx { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[0], asset.Swap, 2*lotSize, swapFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*lotSize, swapFees, false), }, orderUpdate: newOrderUpdate(orderIDs[0], 3*basePerLot, 0, 0, 0, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[0], nil, nil, 2*lotSize, rate1)), @@ -2949,8 +2956,8 @@ func TestDEXTrade(t *testing.T) { }, // Second order has a match and sends swap tx { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[1], asset.Swap, 3*lotSize, swapFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*lotSize, swapFees, false), }, orderUpdate: newOrderUpdate(orderIDs[1], 2*basePerLot, 0, 0, 0, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[1], nil, nil, 3*lotSize, rate2)), @@ -2964,9 +2971,9 @@ func TestDEXTrade(t *testing.T) { }, // First order swap is confirmed, and redemption is sent { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[0], asset.Swap, 2*lotSize, swapFees, true), - newWalletTx(coinIDs[2], asset.Redeem, 2*quoteLot1, redeemFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*lotSize, swapFees, true), + coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*quoteLot1, redeemFees, false), }, orderUpdate: newOrderUpdate(orderIDs[0], 3*basePerLot, 0, 0, 0, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)), @@ -2980,8 +2987,8 @@ func TestDEXTrade(t *testing.T) { }, // First order redemption confirmed { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[2], asset.Redeem, b2q(5e7, 2e6), redeemFees, true), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, b2q(5e7, 2e6), redeemFees, true), }, orderUpdate: newOrderUpdate(orderIDs[0], 3*basePerLot, 0, 0, 0, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)), @@ -3007,10 +3014,10 @@ func TestDEXTrade(t *testing.T) { }, // Second order second match, swap sent, and first match refunded { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[1], asset.Swap, 3*lotSize, swapFees, true), - newWalletTx(coinIDs[3], asset.Refund, 3*lotSize, refundFees, false), - newWalletTx(coinIDs[4], asset.Swap, 2*lotSize, swapFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*lotSize, swapFees, true), + coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*lotSize, refundFees, false), + coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*lotSize, swapFees, false), }, orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 0, 0, order.OrderStatusExecuted, false, newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3*lotSize, rate2), @@ -3025,10 +3032,10 @@ func TestDEXTrade(t *testing.T) { }, // Second order second match redeemed and confirmed, first match refund confirmed { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[3], asset.Refund, 3*lotSize, refundFees, true), - newWalletTx(coinIDs[4], asset.Swap, 2*lotSize, swapFees, true), - newWalletTx(coinIDs[5], asset.Redeem, 2*quoteLot2, redeemFees, true), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*lotSize, refundFees, true), + coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*lotSize, swapFees, true), + coinIDs[5]: newWalletTx(coinIDs[5], asset.Redeem, 2*quoteLot2, redeemFees, true), }, orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 0, 0, order.OrderStatusExecuted, true, newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3e6, 6e7), @@ -3065,8 +3072,8 @@ func TestDEXTrade(t *testing.T) { updatesAndBalances: []*updatesAndBalances{ // First order has a match and sends a swap tx { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[0], asset.Swap, 2*quoteLot1, swapFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*quoteLot1, swapFees, false), }, orderUpdate: newOrderUpdate(orderIDs[0], 3*quotePerLot1, 0, 0, 0, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[0], nil, nil, 2*lotSize, rate1)), @@ -3080,8 +3087,8 @@ func TestDEXTrade(t *testing.T) { }, // Second order has a match and sends swap tx { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[1], asset.Swap, 3*quoteLot2, swapFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*quoteLot2, swapFees, false), }, orderUpdate: newOrderUpdate(orderIDs[1], 2*quotePerLot2, 0, 0, 0, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[1], nil, nil, 3*lotSize, rate2)), @@ -3095,9 +3102,9 @@ func TestDEXTrade(t *testing.T) { }, // First order swap is confirmed, and redemption is sent { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[0], asset.Swap, 2*quoteLot1, swapFees, true), - newWalletTx(coinIDs[2], asset.Redeem, 2*lotSize, redeemFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*quoteLot1, swapFees, true), + coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*lotSize, redeemFees, false), }, orderUpdate: newOrderUpdate(orderIDs[0], 3*quotePerLot1, 0, 0, 0, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*quoteLot1, rate1)), @@ -3111,8 +3118,8 @@ func TestDEXTrade(t *testing.T) { }, // First order redemption confirmed { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[2], asset.Redeem, 2*lotSize, redeemFees, true), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*lotSize, redeemFees, true), }, orderUpdate: newOrderUpdate(orderIDs[0], 3*quotePerLot1, 0, 0, 0, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)), @@ -3138,10 +3145,10 @@ func TestDEXTrade(t *testing.T) { }, // Second order second match, swap sent, and first match refunded { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[1], asset.Swap, 3*quoteLot2, swapFees, true), - newWalletTx(coinIDs[3], asset.Refund, 3*quoteLot2, refundFees, false), - newWalletTx(coinIDs[4], asset.Swap, 2*quoteLot2, swapFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*quoteLot2, swapFees, true), + coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*quoteLot2, refundFees, false), + coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*quoteLot2, swapFees, false), }, orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 0, 0, order.OrderStatusExecuted, false, newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3*lotSize, rate2), @@ -3156,10 +3163,10 @@ func TestDEXTrade(t *testing.T) { }, // Second order second match redeemed and confirmed, first match refund confirmed { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[3], asset.Refund, 3*quoteLot2, refundFees, true), - newWalletTx(coinIDs[4], asset.Swap, 2*quoteLot2, swapFees, true), - newWalletTx(coinIDs[5], asset.Redeem, 2*lotSize, redeemFees, true), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*quoteLot2, refundFees, true), + coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*quoteLot2, swapFees, true), + coinIDs[5]: newWalletTx(coinIDs[5], asset.Redeem, 2*lotSize, redeemFees, true), }, orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 0, 0, order.OrderStatusExecuted, true, newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3*lotSize, rate2), @@ -3204,8 +3211,8 @@ func TestDEXTrade(t *testing.T) { updatesAndBalances: []*updatesAndBalances{ // First order has a match and sends a swap tx { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[0], asset.Swap, 2*lotSize, swapFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*lotSize, swapFees, false), }, orderUpdate: newOrderUpdate(orderIDs[0], 3*basePerLot, 0, 5*redeemFees, 5*refundFees, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[0], nil, nil, 2*lotSize, rate1)), @@ -3220,8 +3227,8 @@ func TestDEXTrade(t *testing.T) { }, // Second order has a match and sends swap tx { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[1], asset.Swap, 3*lotSize, swapFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*lotSize, swapFees, false), }, orderUpdate: newOrderUpdate(orderIDs[1], 2*basePerLot, 0, 5*redeemFees, 5*refundFees, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[1], nil, nil, 3*lotSize, rate2)), @@ -3236,9 +3243,9 @@ func TestDEXTrade(t *testing.T) { }, // First order swap is confirmed, and redemption is sent { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[0], asset.Swap, 2*lotSize, swapFees, true), - newWalletTx(coinIDs[2], asset.Redeem, 2*quoteLot1, redeemFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*lotSize, swapFees, true), + coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*quoteLot1, redeemFees, false), }, orderUpdate: newOrderUpdate(orderIDs[0], 3*basePerLot, 0, 3*redeemFees, 3*refundFees, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)), @@ -3253,8 +3260,8 @@ func TestDEXTrade(t *testing.T) { }, // First order redemption confirmed { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[2], asset.Redeem, 2*quoteLot1, redeemFees, true), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*quoteLot1, redeemFees, true), }, orderUpdate: newOrderUpdate(orderIDs[0], 3*basePerLot, 0, 3*redeemFees, 3*refundFees, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)), @@ -3282,10 +3289,10 @@ func TestDEXTrade(t *testing.T) { }, // Second order second match, swap sent, and first match refunded { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[1], asset.Swap, 3*lotSize, swapFees, true), - newWalletTx(coinIDs[3], asset.Refund, 3*lotSize, refundFees, false), - newWalletTx(coinIDs[4], asset.Swap, 2*lotSize, swapFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*lotSize, swapFees, true), + coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*lotSize, refundFees, false), + coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*lotSize, swapFees, false), }, orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 2*redeemFees, 2*refundFees, order.OrderStatusExecuted, false, newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3*lotSize, rate2), @@ -3301,10 +3308,10 @@ func TestDEXTrade(t *testing.T) { }, // Second order second match redeemed and confirmed, first match refund confirmed { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[3], asset.Refund, 3*lotSize, refundFees, true), - newWalletTx(coinIDs[4], asset.Swap, 2*lotSize, swapFees, true), - newWalletTx(coinIDs[5], asset.Redeem, 2*quoteLot2, redeemFees, true), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*lotSize, refundFees, true), + coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*lotSize, swapFees, true), + coinIDs[5]: newWalletTx(coinIDs[5], asset.Redeem, 2*quoteLot2, redeemFees, true), }, orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 0, 0, order.OrderStatusExecuted, true, newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 0, 0), newMatchUpdate(&coinIDs[4], &coinIDs[5], nil, 0, 0)), stats: &RunStats{ @@ -3347,8 +3354,8 @@ func TestDEXTrade(t *testing.T) { updatesAndBalances: []*updatesAndBalances{ // First order has a match and sends a swap tx { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[0], asset.Swap, 2*quoteLot1, swapFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*quoteLot1, swapFees, false), }, orderUpdate: newOrderUpdate(orderIDs[0], 3*quoteLot1, 3*buyFees, 5*redeemFees, 5*refundFees, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[0], nil, nil, 2*lotSize, rate1)), @@ -3363,8 +3370,8 @@ func TestDEXTrade(t *testing.T) { }, // Second order has a match and sends swap tx { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[1], asset.Swap, 3*quoteLot2, swapFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*quoteLot2, swapFees, false), }, orderUpdate: newOrderUpdate(orderIDs[1], 2*quoteLot2, 2*buyFees, 5*redeemFees, 5*refundFees, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[1], nil, nil, 3*lotSize, rate2)), @@ -3379,9 +3386,9 @@ func TestDEXTrade(t *testing.T) { }, // First order swap is confirmed, and redemption is sent { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[0], asset.Swap, 2*quoteLot1, swapFees, true), - newWalletTx(coinIDs[2], asset.Redeem, 2*lotSize, redeemFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*quoteLot1, swapFees, true), + coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*lotSize, redeemFees, false), }, orderUpdate: newOrderUpdate(orderIDs[0], 3*quoteLot1, 3*buyFees, 3*redeemFees, 3*refundFees, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)), @@ -3396,8 +3403,8 @@ func TestDEXTrade(t *testing.T) { }, // First order redemption confirmed { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[2], asset.Redeem, 2*lotSize, redeemFees, true), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*lotSize, redeemFees, true), }, orderUpdate: newOrderUpdate(orderIDs[0], 3*quoteLot1, 3*buyFees, 3*redeemFees, 3*refundFees, order.OrderStatusBooked, false, newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)), @@ -3425,10 +3432,10 @@ func TestDEXTrade(t *testing.T) { }, // Second order second match, swap sent, and first match refunded { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[1], asset.Swap, 3*quoteLot2, swapFees, true), - newWalletTx(coinIDs[3], asset.Refund, 3*quoteLot2, refundFees, false), - newWalletTx(coinIDs[4], asset.Swap, 2*quoteLot2, swapFees, false), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*quoteLot2, swapFees, true), + coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*quoteLot2, refundFees, false), + coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*quoteLot2, swapFees, false), }, orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 2*redeemFees, 2*refundFees, order.OrderStatusExecuted, false, newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3*lotSize, rate2), @@ -3444,10 +3451,10 @@ func TestDEXTrade(t *testing.T) { }, // Second order second match redeemed and confirmed, first match refund confirmed { - txUpdates: []*asset.WalletTransaction{ - newWalletTx(coinIDs[3], asset.Refund, 3*quoteLot2, refundFees, true), - newWalletTx(coinIDs[4], asset.Swap, 2*quoteLot2, swapFees, true), - newWalletTx(coinIDs[5], asset.Redeem, 2*lotSize, redeemFees, true), + txUpdates: map[string]*asset.WalletTransaction{ + coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*quoteLot2, refundFees, true), + coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*quoteLot2, swapFees, true), + coinIDs[5]: newWalletTx(coinIDs[5], asset.Redeem, 2*lotSize, redeemFees, true), }, orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 0, 0, order.OrderStatusExecuted, true, newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3*lotSize, rate2), @@ -3462,6 +3469,64 @@ func TestDEXTrade(t *testing.T) { }, }, }, + { + name: "non dynamic swapper, sell, shared swap and redeem txs", + initialBalances: map[uint32]uint64{ + 42: 1e8, + 0: 1e8, + }, + sell: true, + baseID: 42, + quoteID: 0, + placements: []*TradePlacement{ + {Lots: 5, Rate: rate1}, + }, + initialLockedFunds: []*orderLockedFunds{ + newOrderLockedFunds(orderIDs[0], basePerLot*5, 0, 0, 0), + }, + postTradeBalances: map[uint32]*BotBalance{ + 42: {1e8 - 5*basePerLot, 5 * basePerLot, 0, 0}, + 0: {1e8, 0, 0, 0}, + }, + updatesAndBalances: []*updatesAndBalances{ + // Order has two matches, sends one swap tx for both + { + txUpdates: map[string]*asset.WalletTransaction{ + *suffixedCoinID(coinIDs[0], 0): newWalletTx(coinIDs[0], asset.Swap, 5*lotSize, swapFees, false), + *suffixedCoinID(coinIDs[0], 1): newWalletTx(coinIDs[0], asset.Swap, 5*lotSize, swapFees, false), + }, + orderUpdate: newOrderUpdate(orderIDs[0], 0, 0, 0, 0, order.OrderStatusBooked, false, + newMatchUpdate(suffixedCoinID(coinIDs[0], 0), nil, nil, 2*lotSize, rate1), + newMatchUpdate(suffixedCoinID(coinIDs[0], 1), nil, nil, 3*lotSize, rate1), + ), + stats: &RunStats{ + DEXBalances: map[uint32]*BotBalance{ + 42: {1e8 - 5*lotSize - swapFees, 0, 0, 0}, + 0: {1e8, 0, 5 * quoteLot1, 0}, + }, + }, + numPendingTrades: 1, + }, + // Both matches redeemed with same tx + { + txUpdates: map[string]*asset.WalletTransaction{ + *suffixedCoinID(coinIDs[1], 0): newWalletTx(coinIDs[1], asset.Redeem, 5*quoteLot1, redeemFees, true), + *suffixedCoinID(coinIDs[1], 1): newWalletTx(coinIDs[1], asset.Redeem, 5*quoteLot1, redeemFees, true), + }, + orderUpdate: newOrderUpdate(orderIDs[0], 0, 0, 0, 0, order.OrderStatusExecuted, true, + newMatchUpdate(suffixedCoinID(coinIDs[0], 0), suffixedCoinID(coinIDs[1], 0), nil, 2*lotSize, rate1), + newMatchUpdate(suffixedCoinID(coinIDs[0], 1), suffixedCoinID(coinIDs[1], 1), nil, 3*lotSize, rate1), + ), + stats: &RunStats{ + DEXBalances: map[uint32]*BotBalance{ + 42: {1e8 - 5*lotSize - swapFees, 0, 0, 0}, + 0: {1e8 + 5*quoteLot1 - redeemFees, 0, 0, 0}, + }, + }, + numPendingTrades: 0, + }, + }, + }, } runTest := func(test *test) { @@ -3572,7 +3637,8 @@ func TestDEXTrade(t *testing.T) { for i, update := range test.updatesAndBalances { tCore.walletTxsMtx.Lock() - for _, txUpdate := range update.txUpdates { + for coinID, txUpdate := range update.txUpdates { + tCore.walletTxs[coinID] = txUpdate tCore.walletTxs[txUpdate.ID] = txUpdate } tCore.walletTxsMtx.Unlock() @@ -5031,9 +5097,12 @@ func TestRefreshPendingEvents(t *testing.T) { }, } pord := &pendingDEXOrder{ - swaps: map[string]*asset.WalletTransaction{}, - redeems: map[string]*asset.WalletTransaction{}, - refunds: map[string]*asset.WalletTransaction{}, + swaps: map[string]*asset.WalletTransaction{}, + redeems: map[string]*asset.WalletTransaction{}, + refunds: map[string]*asset.WalletTransaction{}, + swapCoinIDToTxID: map[string]string{}, + redeemCoinIDToTxID: map[string]string{}, + refundCoinIDToTxID: map[string]string{}, } adaptor.pendingDEXOrders[dexOrderID] = pord pord.state.Store(&dexOrderState{ diff --git a/client/mm/mm.go b/client/mm/mm.go index c4f69b9076..b28852d13e 100644 --- a/client/mm/mm.go +++ b/client/mm/mm.go @@ -1213,7 +1213,7 @@ func (m *MarketMaker) updateDEXOrderEvent(mkt *MarketWithHost, event *MarketMaki return nil, fmt.Errorf("error fetching order: %v", err) } - swapIDs, redeemIDs, refundIDs := orderTransactions(o) + swapIDs, redeemIDs, refundIDs := orderCoinIDs(o) fromAsset, _, toAsset, _ := orderAssets(mkt.BaseID, mkt.QuoteID, o.Sell) swaps := make(map[string]*asset.WalletTransaction, len(swapIDs)) redeems := make(map[string]*asset.WalletTransaction, len(redeemIDs)) @@ -1221,21 +1221,21 @@ func (m *MarketMaker) updateDEXOrderEvent(mkt *MarketWithHost, event *MarketMaki allTxs := make([]*asset.WalletTransaction, 0, len(orderEvent.Transactions)) pendingTx := false - processTxs := func(assetID uint32, txIDs map[string]bool, txs map[string]*asset.WalletTransaction) { - for txid := range txIDs { - tx := findEventTx(txid) + processTxs := func(assetID uint32, coinIDs map[string]bool, txs map[string]*asset.WalletTransaction) { + for coinID := range coinIDs { + tx := findEventTx(coinID) if tx == nil || !tx.Confirmed { var err error - tx, err = m.core.WalletTransaction(assetID, txid) + tx, err = m.core.WalletTransaction(assetID, coinID) if err != nil { - m.log.Errorf("Error fetching transaction %s for %s: %v", txid, mkt, err) + m.log.Errorf("Error fetching transaction %s for %s: %v", coinID, mkt, err) pendingTx = true continue } } - txs[txid] = tx + txs[tx.ID] = tx allTxs = append(allTxs, tx) pendingTx = pendingTx || !tx.Confirmed }