From f23f5ee4fa830b29c45ced5f30c1837c181ba043 Mon Sep 17 00:00:00 2001 From: martonp Date: Mon, 7 Mar 2022 16:43:42 -0500 Subject: [PATCH 01/13] client/core/btc/ui: Accelerate Orders using CPFP Adds support for Accelerating BTC transactions using the Child-Pays-For-Parent technique. Adds a new `Accelerator` wallet trait which includes three functions: `AccelerateOrder` which creates a new transaction sending money to the wallet's change address with a high fee in order to expedite mining, `PreAccelerate`, which gives guidelines to the user, and `AccelerationEstimate` which returns how much it will cost to accelerate the order to a certain fee rate. On the UI, the orders page is updated with a button which shows up if the order is able to be accelerated. When clicking the button, a popup shows up which allows the user to accelerate an order. --- client/asset/btc/btc.go | 474 ++++++++++- client/asset/btc/btc_test.go | 800 +++++++++++++++++- client/asset/btc/rpcclient.go | 2 +- client/asset/btc/spv.go | 7 +- client/asset/btc/spv_test.go | 69 +- client/asset/btc/wallettypes.go | 1 + client/asset/interface.go | 32 + client/core/core.go | 137 +++ client/core/core_test.go | 184 ++++ client/core/types.go | 65 +- client/core/wallet.go | 32 + client/db/bolt/db.go | 32 + client/db/types.go | 3 + client/webserver/api.go | 79 ++ client/webserver/http.go | 1 - client/webserver/live_test.go | 10 +- client/webserver/locales/en-us.go | 8 + client/webserver/site/src/css/order.scss | 36 + client/webserver/site/src/html/order.tmpl | 73 ++ client/webserver/site/src/js/order.ts | 157 +++- client/webserver/site/src/js/orderutil.ts | 92 +- client/webserver/site/src/js/registry.ts | 1 + .../site/src/localized_html/en-US/order.tmpl | 73 ++ .../site/src/localized_html/pl-PL/order.tmpl | 73 ++ .../site/src/localized_html/pt-BR/order.tmpl | 73 ++ .../site/src/localized_html/zh-CN/order.tmpl | 73 ++ client/webserver/webserver.go | 6 + client/webserver/webserver_test.go | 9 + dex/networks/btc/script.go | 11 +- 29 files changed, 2500 insertions(+), 113 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index c664ddc7dc..53505bcfc9 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -61,7 +61,7 @@ const ( // splitTxBaggageSegwit it the analogue of splitTxBaggage for segwit. // We include the 2 bytes for marker and flag. splitTxBaggageSegwit = dexbtc.MinimumTxOverhead + 2*dexbtc.P2WPKHOutputSize + - dexbtc.RedeemP2WPKHInputSize + ((dexbtc.RedeemP2WPKHInputWitnessWeight + 2 + 3) / 4) + dexbtc.RedeemP2PWKHInputTotalSize walletTypeLegacy = "" walletTypeRPC = "bitcoindRPC" @@ -612,6 +612,7 @@ type ExchangeWalletFullNode struct { // Check that wallets satisfy their supported interfaces. var _ asset.Wallet = (*baseWallet)(nil) +var _ asset.Accelerator = (*baseWallet)(nil) var _ asset.Rescanner = (*ExchangeWalletSPV)(nil) var _ asset.FeeRater = (*ExchangeWalletFullNode)(nil) var _ asset.LogFiler = (*ExchangeWalletSPV)(nil) @@ -1756,7 +1757,7 @@ func (btc *baseWallet) split(value uint64, lots uint64, outputs []*output, func (btc *baseWallet) splitBaggageFees(maxFeeRate uint64) (swapInputSize, baggage uint64) { if btc.segwit { baggage = maxFeeRate * splitTxBaggageSegwit - swapInputSize = dexbtc.RedeemP2WPKHInputSize + ((dexbtc.RedeemP2WPKHInputWitnessWeight + 2 + 3) / 4) + swapInputSize = dexbtc.RedeemP2PWKHInputTotalSize return } baggage = maxFeeRate * splitTxBaggage @@ -1923,6 +1924,475 @@ func (btc *baseWallet) fundedTx(coins asset.Coins) (*wire.MsgTx, uint64, []outPo return baseTx, totalIn, pts, nil } +// AccelerateOrder uses the Child-Pays-For-Parent technique to accelerate a +// chain of swap transactions and previous accelerations. It broadcasts a new +// transaction with a fee high enough so that the average fee of all the +// unconfirmed transactions in the chain and the new transaction will have +// an average fee rate of newFeeRate. requiredForRemainingSwaps is passed +// in to ensure that the new change coin will have enough funds to initiate +// the additional swaps that will be required to complete the order. +func (btc *baseWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { + btc.fundingMtx.Lock() + defer btc.fundingMtx.Unlock() + + signedTx, newChange, _, err := btc.signedAccelerationTx(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) + if err != nil { + return nil, "", err + } + + err = btc.broadcastTx(signedTx) + if err != nil { + return nil, "", err + } + + var newChangeCoin asset.Coin + if newChange != nil { + newChangeCoin = newChange + + // Checking required for remaining swaps > 0 because this ensures if the previous change + // was locked, this one will also be locked. If requiredForRemainingSwaps = 0, but the + // change was locked, signedAccelerationTx would have returned an error since this means + // that the change was locked by another order. + if requiredForRemainingSwaps > 0 { + err = btc.node.lockUnspent(false, []*output{newChange}) + if err != nil { + // The transaction is already broadcasted, so don't fail now. + btc.log.Errorf("failed to lock change output: %v", err) + } + + // Log it as a fundingCoin, since it is expected that this will be + // chained into further matches. + btc.fundingCoins[newChange.pt] = &utxo{ + txHash: newChange.txHash(), + vout: newChange.vout(), + address: newChange.String(), + amount: newChange.value, + } + } + } + + return newChangeCoin, signedTx.TxHash().String(), err +} + +// AccelerationEstimate takes the same parameters as AccelerateOrder, but +// instead of broadcasting the acceleration transaction, it just returns +// the amount of funds that will need to be spent in order to increase the +// average fee rate to the desired amount. +func (btc *baseWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) { + btc.fundingMtx.RLock() + defer btc.fundingMtx.RUnlock() + _, _, fee, err := btc.signedAccelerationTx(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) + if err != nil { + return 0, err + } + + return fee, nil +} + +// PreAccelerate returns the current average fee rate of the unmined swap initiation +// and acceleration transactions, and also returns a suggested range that the +// fee rate should be increased to in order to expedite mining. +func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange asset.XYRange, err error) { + makeError := func(err error) (uint64, asset.XYRange, error) { + return 0, asset.XYRange{}, err + } + + changeTxHash, changeVout, err := decodeCoinID(changeCoin) + if err != nil { + return makeError(err) + } + + can, err := btc.changeCanBeAccelerated(changeTxHash, changeVout, requiredForRemainingSwaps) + if err != nil { + return makeError(err) + } + if !can { + return makeError(errors.New("change cannot be accelerated")) + } + + sortedTxChain, err := btc.sortedTxChain(swapCoins, accelerationCoins, changeCoin) + if err != nil { + return makeError(fmt.Errorf("failed to sort swap chain: %w", err)) + } + + var swapTxsSize, feesAlreadyPaid uint64 + for _, tx := range sortedTxChain { + if tx.Confirmations > 0 { + continue + } + feesAlreadyPaid += toSatoshi(math.Abs(tx.Fee)) + msgTx, err := msgTxFromBytes(tx.Hex) + if err != nil { + return makeError(err) + } + swapTxsSize += dexbtc.MsgTxVBytes(msgTx) + } + + btc.fundingMtx.RLock() + utxos, _, utxosVal, err := btc.spendableUTXOs(1) + btc.fundingMtx.RUnlock() + if err != nil { + return makeError(err) + } + + var coins asset.Coins + for _, utxo := range utxos { + coins = append(coins, newOutput(utxo.txHash, utxo.vout, utxo.amount)) + } + + changeOutput, err := btc.lookupOutput(changeTxHash, changeVout) + if err != nil { + return makeError(err) + } + + tx, _, _, err := btc.fundedTx(append(coins, changeOutput)) + if err != nil { + return makeError(err) + } + tx, err = btc.node.signTx(tx) + if err != nil { + return makeError(err) + } + + newChangeTxSize := dexbtc.MsgTxVBytes(tx) + if requiredForRemainingSwaps > 0 { + if btc.segwit { + newChangeTxSize += dexbtc.P2WPKHOutputSize + } else { + newChangeTxSize += dexbtc.P2PKHOutputSize + } + } + + maxRate := (changeOutput.value + feesAlreadyPaid + utxosVal - requiredForRemainingSwaps) / (newChangeTxSize + swapTxsSize) + currentRate = feesAlreadyPaid / swapTxsSize + + if maxRate <= currentRate { + return makeError(fmt.Errorf("cannot accelerate, max rate %v <= current rate %v", maxRate, currentRate)) + } + + maxSuggestion := currentRate * 5 + if feeSuggestion*5 > maxSuggestion { + maxSuggestion = feeSuggestion * 5 + } + if maxRate < maxSuggestion { + maxSuggestion = maxRate + } + + suggestedRange = asset.XYRange{ + Start: asset.XYRangePoint{ + Label: "Min", + X: float64(currentRate+1) / float64(currentRate), + Y: float64(currentRate + 1), + }, + End: asset.XYRangePoint{ + Label: "Max", + X: float64(maxSuggestion) / float64(currentRate), + Y: float64(maxSuggestion), + }, + XUnit: "X", + YUnit: btc.walletInfo.UnitInfo.AtomicUnit + "/" + btc.sizeUnit(), + } + + return currentRate, suggestedRange, nil +} + +// signedAccelerationTx returns a signed transaction that sends funds to a +// change address controlled by this wallet. This new transaction will have +// a fee high enough to make the average fee of the unmined swapCoins and +// accelerationTxs to be newFeeRate. +func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (*wire.MsgTx, *output, uint64, error) { + makeError := func(err error) (*wire.MsgTx, *output, uint64, error) { + return nil, nil, 0, err + } + + changeTxHash, changeVout, err := decodeCoinID(changeCoin) + if err != nil { + return makeError(err) + } + + can, err := btc.changeCanBeAccelerated(changeTxHash, changeVout, requiredForRemainingSwaps) + if err != nil { + return makeError(err) + } + if !can { + return makeError(errors.New("change cannot be accelerated")) + } + + sortedTxChain, err := btc.sortedTxChain(swapCoins, accelerationCoins, changeCoin) + if err != nil { + return makeError(fmt.Errorf("failed to sort swap chain: %w", err)) + } + + changeOutput, err := btc.lookupOutput(changeTxHash, changeVout) + if err != nil { + return makeError(nil) + } + + additionalFeesRequired, err := btc.additionalFeesRequired(sortedTxChain, newFeeRate) + if err != nil { + return makeError(err) + } + + txSize := dexbtc.MinimumTxOverhead + if btc.segwit { + txSize += dexbtc.RedeemP2PWKHInputTotalSize + } else { + txSize += dexbtc.RedeemP2PKHInputSize + } + if requiredForRemainingSwaps > 0 { + if btc.segwit { + txSize += dexbtc.P2WPKHOutputSize + } else { + txSize += dexbtc.P2PKHOutputSize + } + } + fundsRequired := additionalFeesRequired + requiredForRemainingSwaps + uint64(txSize)*newFeeRate + + var additionalInputs asset.Coins + if fundsRequired > changeOutput.value { + // If change not enough, need to use other UTXOs. + utxos, _, _, err := btc.spendableUTXOs(1) + if err != nil { + return makeError(err) + } + + _, _, additionalInputs, _, _, _, err = fund(utxos, func(inputSize, inputsVal uint64) bool { + txSize := dexbtc.MinimumTxOverhead + inputSize + + // input is the change input that we must use + if btc.segwit { + txSize += dexbtc.RedeemP2PWKHInputTotalSize + } else { + txSize += dexbtc.RedeemP2PKHInputSize + } + + if requiredForRemainingSwaps > 0 { + if btc.segwit { + txSize += dexbtc.P2WPKHOutputSize + } else { + txSize += dexbtc.P2PKHOutputSize + } + } + + totalFees := additionalFeesRequired + txSize*newFeeRate + return totalFees+requiredForRemainingSwaps <= inputsVal+changeOutput.value + }) + if err != nil { + return makeError(fmt.Errorf("failed to fund acceleration tx: %w", err)) + } + } + + baseTx, totalIn, _, err := btc.fundedTx(append(additionalInputs, changeOutput)) + if err != nil { + return makeError(err) + } + + changeAddr, err := btc.node.changeAddress() + if err != nil { + return makeError(fmt.Errorf("error creating change address: %w", err)) + } + + tx, output, txFee, err := btc.signTxAndAddChange(baseTx, changeAddr, totalIn, additionalFeesRequired, newFeeRate) + if err != nil { + return makeError(err) + } + + return tx, output, txFee + additionalFeesRequired, nil +} + +// lookupOutput looks up the value of a transaction output and creates an +// output. +func (btc *baseWallet) lookupOutput(txHash *chainhash.Hash, vout uint32) (*output, error) { + getTxResult, err := btc.node.getWalletTransaction(txHash) + if err != nil { + return nil, err + } + tx, err := msgTxFromBytes(getTxResult.Hex) + if err != nil { + return nil, err + } + if len(tx.TxOut) <= int(vout) { + return nil, fmt.Errorf("txId %x only has %d outputs. tried to access index %d", + &txHash, len(tx.TxOut), vout) + } + + value := tx.TxOut[vout].Value + return newOutput(txHash, vout, uint64(value)), nil +} + +// changeCanBeAccelerated will return an error if the change cannot be accelerated. +func (btc *baseWallet) changeCanBeAccelerated(changeTxHash *chainhash.Hash, changeVout uint32, requiredForRemainingSwaps uint64) (bool, error) { + lockedUtxos, err := btc.node.listLockUnspent() + if err != nil { + return false, err + } + + var changeIsLocked bool + for _, utxo := range lockedUtxos { + if utxo.TxID == changeTxHash.String() && utxo.Vout == changeVout { + changeIsLocked = true + break + } + } + + if changeIsLocked && requiredForRemainingSwaps == 0 { + btc.log.Error("change cannot be accelerated because it is locked by another order") + return false, nil + } + + if !changeIsLocked { + utxos, err := btc.node.listUnspent() + if err != nil { + return false, err + } + + var changeIsUnspent bool + for _, utxo := range utxos { + if utxo.TxID == changeTxHash.String() && utxo.Vout == changeVout { + changeIsUnspent = true + break + } + } + + if !changeIsUnspent { + btc.log.Error("change cannot be accelerated because it has already been spent") + return false, nil + } + } + + return true, nil +} + +// sortedTxChain takes a list of swap coins, acceleration tx IDs, and a change +// coin, and returns a sorted list of transactions. An error is returned if a +// sorted list of transactions that ends with a transaction containing the change +// cannot be created using each of the swap coins and acceleration transactions. +func (btc *baseWallet) sortedTxChain(swapCoins, accelerationCoins []dex.Bytes, change dex.Bytes) ([]*GetTransactionResult, error) { + txChain := make([]*GetTransactionResult, 0, len(swapCoins)+len(accelerationCoins)) + if len(swapCoins) == 0 { + return txChain, nil + } + + for _, coinID := range swapCoins { + txHash, _, err := decodeCoinID(coinID) + if err != nil { + return nil, err + } + getTxRes, err := btc.node.getWalletTransaction(txHash) + if err != nil { + return nil, err + } + txChain = append(txChain, getTxRes) + } + + for _, coinID := range accelerationCoins { + txHash, _, err := decodeCoinID(coinID) + if err != nil { + return nil, err + } + getTxRes, err := btc.node.getWalletTransaction(txHash) + if err != nil { + return nil, err + } + txChain = append(txChain, getTxRes) + } + + msgTxs := make(map[string]*wire.MsgTx, len(txChain)) + for _, gtr := range txChain { + msgTx, err := msgTxFromBytes(gtr.Hex) + if err != nil { + return nil, err + } + msgTxs[gtr.TxID] = msgTx + } + + changeTxHash, _, err := decodeCoinID(change) + if err != nil { + return nil, err + } + + swap := func(i, j int) { + temp := txChain[j] + txChain[j] = txChain[i] + txChain[i] = temp + } + + // sourcesOfTxInputs finds the transaction IDs that the inputs + // for a certain transaction come from. If all the transactions + // are from the same order, only the first transaction in + // the chain can have multiple inputs. + sourcesOfTxInputs := func(txID string) (map[string]bool, error) { + lastTx, found := msgTxs[txID] + if !found { + // this should never happen + return nil, fmt.Errorf("could not find tx with id: %v", txID) + } + + inputSources := make(map[string]bool, len(lastTx.TxIn)) + for _, in := range lastTx.TxIn { + inputSources[in.PreviousOutPoint.Hash.String()] = true + } + return inputSources, nil + } + + // The last tx in the chain must have the same tx hash as the change. + for i, tx := range txChain { + if tx.TxID == changeTxHash.String() { + swap(i, len(txChain)-1) + break + } + if i == len(txChain)-1 { + return nil, fmt.Errorf("could not find tx containing change coin") + } + } + + // We work backwards to find each element of the swap chain. + for i := len(txChain) - 2; i >= 0; i-- { + lastTxInputs, err := sourcesOfTxInputs(txChain[i+1].TxID) + if err != nil { + return nil, err + } + + for j, getTxRes := range txChain { + if lastTxInputs[getTxRes.TxID] { + swap(i, j) + break + } + if j == len(txChain)-1 { + return nil, errors.New("could not find previous element of sorted chain") + } + } + } + + return txChain, nil +} + +// additionalFeesRequired calculates the additional satoshis that need to be +// sent to miners in order to increase the average fee rate of unconfirmed +// transactions to newFeeRate. +func (btc *baseWallet) additionalFeesRequired(txs []*GetTransactionResult, newFeeRate uint64) (uint64, error) { + var totalTxSize, feesAlreadyPaid uint64 + for _, tx := range txs { + if tx.Confirmations > 0 { + continue + } + msgTx, err := msgTxFromBytes(tx.Hex) + if err != nil { + return 0, err + } + totalTxSize += dexbtc.MsgTxVBytes(msgTx) + feesAlreadyPaid += toSatoshi(math.Abs(tx.Fee)) + } + + if feesAlreadyPaid >= totalTxSize*newFeeRate { + return 0, fmt.Errorf("extra fees are not needed. %d would be needed "+ + "for a fee rate of %d, but %d was already paid", + totalTxSize*newFeeRate, newFeeRate, feesAlreadyPaid) + } + + return totalTxSize*newFeeRate - feesAlreadyPaid, nil +} + // Swap sends the swaps in a single transaction and prepares the receipts. The // Receipts returned can be used to refund a failed transaction. The Input coins // are NOT manually unlocked because they're auto-unlocked when the transaction diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index 52d00401e4..7aba01f406 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -19,6 +19,7 @@ import ( "decred.org/dcrdex/client/asset" "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/calc" + "decred.org/dcrdex/dex/encode" dexbtc "decred.org/dcrdex/dex/networks/btc" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcjson" @@ -157,7 +158,10 @@ type testData struct { privKeyForAddr *btcutil.WIF privKeyForAddrErr error birthdayTime time.Time - getTransaction *GetTransactionResult + + // If there is an "any" key in the getTransactionMap, that value will be + // returned for all requests. Otherwise the tx id is looked up. + getTransactionMap map[string]*GetTransactionResult getTransactionErr error getBlockchainInfoErr error @@ -199,6 +203,7 @@ func newTestData() *testData { confsErr: WalletTransactionNotFound, checkpoints: make(map[outPoint]*scanCheckpoint), tipChanged: make(chan struct{}, 1), + getTransactionMap: make(map[string]*GetTransactionResult), } } @@ -419,7 +424,28 @@ func (c *tRawRequester) RawRequest(_ context.Context, method string, params []js } return json.Marshal(c.privKeyForAddr.String()) case methodGetTransaction: - return encodeOrError(c.getTransaction, c.getTransactionErr) + if c.getTransactionErr != nil { + return nil, c.getTransactionErr + } + + c.blockchainMtx.Lock() + defer c.blockchainMtx.Unlock() + var txID string + err := json.Unmarshal(params[0], &txID) + if err != nil { + return nil, err + } + + var txData *GetTransactionResult + if c.getTransactionMap != nil { + if txData = c.getTransactionMap["any"]; txData == nil { + txData = c.getTransactionMap[txID] + } + } + if txData == nil { + return nil, WalletTransactionNotFound + } + return json.Marshal(txData) case methodGetBlockchainInfo: c.blockchainMtx.RLock() defer c.blockchainMtx.RUnlock() @@ -761,17 +787,18 @@ func testAvailableFund(t *testing.T, segwit bool, walletType string) { const blockHeight = 5 blockHash, _ := node.addRawTx(blockHeight, msgTx) - node.getTransaction = &GetTransactionResult{ - BlockHash: blockHash.String(), - BlockIndex: blockHeight, - Details: []*WalletTxDetails{ - { - Amount: float64(lockedVal) / 1e8, - Vout: 1, + node.getTransactionMap = map[string]*GetTransactionResult{ + "any": &GetTransactionResult{ + BlockHash: blockHash.String(), + BlockIndex: blockHeight, + Details: []*WalletTxDetails{ + { + Amount: float64(lockedVal) / 1e8, + Vout: 1, + }, }, - }, - Hex: txBuf.Bytes(), - } + Hex: txBuf.Bytes(), + }} bal, err = wallet.Balance() if err != nil { @@ -1216,7 +1243,9 @@ func testFundingCoins(t *testing.T, segwit bool, walletType string) { }, }, } - node.getTransaction = getTxRes + + node.getTransactionMap = map[string]*GetTransactionResult{ + "any": getTxRes} ensureGood() } @@ -2079,7 +2108,8 @@ func testFindRedemption(t *testing.T, segwit bool, walletType string) { }, Hex: txHex, } - node.getTransaction = getTxRes + node.getTransactionMap = map[string]*GetTransactionResult{ + "any": getTxRes} // Add an intermediate block for good measure. node.addRawTx(contractHeight+1, makeRawTx([]dex.Bytes{otherScript}, inputs)) @@ -2346,11 +2376,12 @@ func testSender(t *testing.T, senderType tSenderType, segwit bool, walletType st txB, _ := serializeMsgTx(tx) node.sendToAddress = txHash.String() - node.getTransaction = &GetTransactionResult{ - BlockHash: blockHash.String(), - BlockIndex: blockHeight, - Hex: txB, - } + node.getTransactionMap = map[string]*GetTransactionResult{ + "any": &GetTransactionResult{ + BlockHash: blockHash.String(), + BlockIndex: blockHeight, + Hex: txB, + }} unspents := []*ListUnspentResult{{ TxID: txHash.String(), @@ -2509,10 +2540,12 @@ func testConfirmations(t *testing.T, segwit bool, walletType string) { node.getCFilterScripts[*blockHash] = [][]byte{pkScript} node.getTransactionErr = nil txB, _ := serializeMsgTx(tx) - node.getTransaction = &GetTransactionResult{ - BlockHash: blockHash.String(), - Hex: txB, - } + + node.getTransactionMap = map[string]*GetTransactionResult{ + "any": &GetTransactionResult{ + BlockHash: blockHash.String(), + Hex: txB, + }} node.getCFilterScripts[*spendingBlockHash] = [][]byte{pkScript} node.walletTxSpent = true @@ -3078,3 +3111,724 @@ func TestPrettyBTC(t *testing.T) { } } } + +// TestAccelerateOrder tests the entire acceleration workflow, including +// AccelerateOrder, PreAccelerate, and AccelerationEstimate +func TestAccelerateOrder(t *testing.T) { + runRubric(t, testAccelerateOrder) +} + +func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { + wallet, node, shutdown, err := tNewWallet(segwit, walletType) + defer shutdown() + if err != nil { + t.Fatal(err) + } + + var blockHash100 chainhash.Hash + copy(blockHash100[:], encode.RandomBytes(32)) + node.verboseBlocks[blockHash100.String()] = &msgBlockWithHeight{height: 100, msgBlock: &wire.MsgBlock{ + Header: wire.BlockHeader{Timestamp: time.Now()}, + }} + node.mainchain[100] = &blockHash100 + + if segwit { + node.changeAddr = tP2WPKHAddr + } else { + node.changeAddr = tP2PKHAddr + } + + node.signFunc = func(tx *wire.MsgTx) { + signFunc(tx, 0, wallet.segwit) + } + + sumFees := func(fees []float64, confs []uint64) uint64 { + var totalFees uint64 + for i, fee := range fees { + if confs[i] == 0 { + totalFees += toSatoshi(fee) + } + } + return totalFees + } + + sumTxSizes := func(txs []*wire.MsgTx, confirmations []uint64) uint64 { + var totalSize uint64 + for i, tx := range txs { + if confirmations[i] == 0 { + totalSize += dexbtc.MsgTxVBytes(tx) + } + } + + return totalSize + } + + loadTxsIntoNode := func(txs []*wire.MsgTx, fees []float64, confs []uint64, node *testData, t *testing.T) { + t.Helper() + if len(txs) != len(fees) || len(txs) != len(confs) { + t.Fatalf("len(txs) = %d, len(fees) = %d, len(confs) = %d", len(txs), len(fees), len(confs)) + } + + serializedTxs := make([][]byte, 0, len(txs)) + for _, tx := range txs { + serializedTx, err := serializeMsgTx(tx) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + serializedTxs = append(serializedTxs, serializedTx) + } + + for i := range txs { + var blockHash string + if confs[i] == 1 { + blockHash = blockHash100.String() + } + node.getTransactionMap[txs[i].TxHash().String()] = &GetTransactionResult{ + TxID: txs[i].TxHash().String(), + Hex: serializedTxs[i], + BlockHash: blockHash, + Fee: fees[i], + Confirmations: confs[i]} + } + } + + // getAccelerationParams returns a chain of 4 swap transactions where the change + // output of the last transaction has a certain value. If addAcceleration true, + // the third transaction will be an acceleration transaction instead of one + // that initiates a swap. + getAccelerationParams := func(changeVal int64, addChangeToUnspent, addAcceleration bool, fees []float64, node *testData) ([]dex.Bytes, []dex.Bytes, dex.Bytes, []*wire.MsgTx) { + txs := make([]*wire.MsgTx, 4) + + // In order to be able to test using the SPV wallet, we need to properly + // set the size of each of the outputs. The SPV wallet will parse these + // transactions and calculate the fee on its own instead of just returning + // what was set in getTransactionMap. + changeOutputAmounts := make([]int64, 4) + changeOutputAmounts[3] = changeVal + swapAmount := int64(2e6) + for i := 2; i >= 0; i-- { + var changeAmount int64 = int64(toSatoshi(fees[i+1])) + changeOutputAmounts[i+1] + if !(i == 1 && addAcceleration) { + changeAmount += swapAmount + } + changeOutputAmounts[i] = changeAmount + } + + // The initial transaction in the chain has multiple inputs. + fundingCoinsTotalOutput := changeOutputAmounts[0] + swapAmount + int64(toSatoshi(fees[0])) + fundingTx := wire.MsgTx{ + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: *tTxHash, + Index: 0, + }}, + {PreviousOutPoint: wire.OutPoint{ + Hash: *tTxHash, + Index: 1, + }}, + }, + TxOut: []*wire.TxOut{{ + Value: fundingCoinsTotalOutput / 2, + }, { + Value: fundingCoinsTotalOutput - fundingCoinsTotalOutput/2, + }}, + } + fudingTxHex, _ := serializeMsgTx(&fundingTx) + node.getTransactionMap[fundingTx.TxHash().String()] = &GetTransactionResult{Hex: fudingTxHex, BlockHash: blockHash100.String()} + + txs[0] = &wire.MsgTx{ + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: fundingTx.TxHash(), + Index: 0, + }}, + {PreviousOutPoint: wire.OutPoint{ + Hash: fundingTx.TxHash(), + Index: 1, + }}, + }, + TxOut: []*wire.TxOut{{ + Value: changeOutputAmounts[0], + }, { + Value: swapAmount, + }}, + } + for i := 1; i < 4; i++ { + txs[i] = &wire.MsgTx{ + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: txs[i-1].TxHash(), + Index: 0, + }}, + }, + TxOut: []*wire.TxOut{{ + Value: changeOutputAmounts[i], + }}, + } + if !(i == 2 && addAcceleration) { + txs[i].TxOut = append(txs[i].TxOut, &wire.TxOut{Value: swapAmount}) + } + } + + swapCoins := make([]dex.Bytes, 0, len(txs)) + accelerationCoins := make([]dex.Bytes, 0, 1) + var changeCoin dex.Bytes + for i, tx := range txs { + hash := tx.TxHash() + + if i == 2 && addAcceleration { + accelerationCoins = append(accelerationCoins, toCoinID(&hash, 0)) + } else { + toCoinID(&hash, 0) + swapCoins = append(swapCoins, toCoinID(&hash, 0)) + } + + if i == len(txs)-1 { + changeCoin = toCoinID(&hash, 0) + if addChangeToUnspent { + node.listUnspent = append(node.listUnspent, &ListUnspentResult{ + TxID: hash.String(), + Vout: 0, + }) + } + } + } + + return swapCoins, accelerationCoins, changeCoin, txs + } + + addUTXOToNode := func(confs uint32, segwit bool, amount uint64, node *testData) { + var scriptPubKey []byte + if segwit { + scriptPubKey = tP2WPKH + } else { + scriptPubKey = tP2PKH + } + + node.listUnspent = append(node.listUnspent, &ListUnspentResult{ + TxID: hex.EncodeToString(encode.RandomBytes(32)), + Address: "1Bggq7Vu5oaoLFV1NNp5KhAzcku83qQhgi", + Amount: toBTC(amount), + Confirmations: confs, + ScriptPubKey: scriptPubKey, + Spendable: true, + Solvable: true, + Safe: true, + }) + + var prevChainHash chainhash.Hash + copy(prevChainHash[:], encode.RandomBytes(32)) + + tx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: prevChainHash, + Index: 0, + }}, + }, + TxOut: []*wire.TxOut{ + { + Value: int64(amount), + }, + }} + unspentTxHex, err := serializeMsgTx(tx) + if err != nil { + panic(fmt.Sprintf("could not serialize: %v", err)) + } + var blockHash string + if confs == 0 { + blockHash = blockHash100.String() + } + + node.getTransactionMap[node.listUnspent[len(node.listUnspent)-1].TxID] = &GetTransactionResult{ + TxID: tx.TxHash().String(), + Hex: unspentTxHex, + BlockHash: blockHash, + Fee: 1e6, + Confirmations: uint64(confs)} + } + + totalInputOutput := func(tx *wire.MsgTx) (uint64, uint64) { + var in, out uint64 + for _, input := range tx.TxIn { + inputGtr, found := node.getTransactionMap[input.PreviousOutPoint.Hash.String()] + if !found { + t.Fatalf("tx id not found: %v", input.PreviousOutPoint.Hash.String()) + } + inputTx, err := msgTxFromHex(inputGtr.Hex.String()) + if err != nil { + t.Fatalf("failed to deserialize tx: %v", err) + } + in += uint64(inputTx.TxOut[input.PreviousOutPoint.Index].Value) + } + + for _, output := range node.sentRawTx.TxOut { + out += uint64(output.Value) + } + + return in, out + } + + calculateChangeTxSize := func(hasChange, segwit bool, numInputs int) uint64 { + baseSize := dexbtc.MinimumTxOverhead + + var inputSize, witnessSize, outputSize int + if segwit { + witnessSize = dexbtc.RedeemP2WPKHInputWitnessWeight*numInputs + 2 + inputSize = dexbtc.RedeemP2WPKHInputSize * numInputs + outputSize = dexbtc.P2WPKHOutputSize + } else { + inputSize = numInputs * dexbtc.RedeemP2PKHInputSize + outputSize = dexbtc.P2PKHOutputSize + } + + baseSize += inputSize + if hasChange { + baseSize += outputSize + } + + txWeight := baseSize*4 + witnessSize + txSize := (txWeight + 3) / 4 + return uint64(txSize) + } + + changeAmount := int64(21350) + fees := []float64{0.00002, 0.000005, 0.00001, 0.00001} + confs := []uint64{0, 0, 0, 0} + _, _, _, txs := getAccelerationParams(changeAmount, false, false, fees, node) + expectedFees := (sumTxSizes(txs, confs)+calculateChangeTxSize(false, segwit, 1))*50 - sumFees(fees, confs) + _, _, _, txs = getAccelerationParams(changeAmount, true, false, fees, node) + expectedFeesWithChange := (sumTxSizes(txs, confs)+calculateChangeTxSize(true, segwit, 1))*50 - sumFees(fees, confs) + + // See dexbtc.IsDust for the source of this dustCoverage voodoo. + var dustCoverage uint64 + if segwit { + dustCoverage = (dexbtc.P2WPKHOutputSize + 41 + (107 / 4)) * 3 * 50 + } else { + dustCoverage = (dexbtc.P2PKHOutputSize + 41 + 107) * 3 * 50 + } + + type utxo struct { + amount uint64 + confs uint32 + } + + tests := []struct { + name string + changeNotInUnspent bool + addPreviousAcceleration bool + numUtxosUsed int + changeAmount int64 + lockChange bool + scrambleSwapCoins bool + expectChange bool + utxos []utxo + fees []float64 + confs []uint64 + requiredForRemainingSwaps uint64 + expectChangeLocked bool + + // needed to test AccelerateOrder and AccelerationEstimate + expectAccelerateOrderErr bool + expectAccelerationEstimateErr bool + newFeeRate uint64 + + // needed to test PreAccelerate + suggestedFeeRate uint64 + expectPreAccelerateErr bool + }{ + { + name: "change not in utxo set", + changeAmount: int64(expectedFees), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + expectAccelerationEstimateErr: true, + expectAccelerateOrderErr: true, + expectPreAccelerateErr: true, + changeNotInUnspent: true, + }, + { + name: "just enough without change", + changeAmount: int64(expectedFees), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: confs, + newFeeRate: 50, + suggestedFeeRate: 30, + }, + { + name: "works with acceleration", + changeAmount: int64(expectedFees), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: confs, + newFeeRate: 50, + addPreviousAcceleration: true, + }, + { + name: "scramble swap coins", + changeAmount: int64(expectedFees), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + scrambleSwapCoins: true, + }, + { + name: "not enough with just change", + changeAmount: int64(expectedFees - 1), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + expectAccelerationEstimateErr: true, + expectAccelerateOrderErr: true, + }, + { + name: "add non dust amount to change", + changeAmount: int64(expectedFeesWithChange + dustCoverage), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + expectChange: true, + }, + { + name: "add less than non dust amount to change", + changeAmount: int64(expectedFeesWithChange + dustCoverage - 1), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + expectChange: false, + }, + { + name: "don't accelerate confirmed transactions", + changeAmount: int64(expectedFeesWithChange + dustCoverage), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: []uint64{1, 1, 0, 0}, + newFeeRate: 50, + expectChange: true, + }, + { + name: "not enough", + changeAmount: int64(expectedFees - 1), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + expectAccelerationEstimateErr: true, + expectAccelerateOrderErr: true, + }, + { + name: "not enough with 0-conf utxo in wallet", + changeAmount: int64(expectedFees - 1), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + expectAccelerationEstimateErr: true, + expectAccelerateOrderErr: true, + utxos: []utxo{{ + confs: 0, + amount: 5e9, + }}, + }, + { + name: "not enough with 1-conf utxo in wallet", + changeAmount: int64(expectedFees - 1), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + numUtxosUsed: 1, + expectChange: true, + utxos: []utxo{{ + confs: 1, + amount: 2e6, + }}, + }, + { + name: "enough with 1-conf utxo in wallet", + changeAmount: int64(expectedFees - 1), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + numUtxosUsed: 1, + expectChange: true, + utxos: []utxo{{ + confs: 1, + amount: 2e6, + }}, + }, + { + name: "not enough for remaining swaps", + changeAmount: int64(expectedFees - 1), + fees: fees, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + numUtxosUsed: 1, + expectAccelerationEstimateErr: true, + expectAccelerateOrderErr: true, + expectChange: true, + requiredForRemainingSwaps: 2e6, + utxos: []utxo{{ + confs: 1, + amount: 2e6, + }}, + }, + { + name: "enough for remaining swaps", + changeAmount: int64(expectedFees - 1), + fees: fees, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + numUtxosUsed: 2, + expectChange: true, + expectChangeLocked: true, + requiredForRemainingSwaps: 2e6, + utxos: []utxo{ + { + confs: 1, + amount: 2e6, + }, + { + confs: 1, + amount: 1e6, + }, + }, + }, + { + name: "locked change, required for remaining > 0", + changeAmount: int64(expectedFees - 1), + fees: fees, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + numUtxosUsed: 2, + expectChange: true, + expectChangeLocked: true, + requiredForRemainingSwaps: 2e6, + lockChange: true, + utxos: []utxo{ + { + confs: 1, + amount: 2e6, + }, + { + confs: 1, + amount: 1e6, + }, + }, + }, + { + name: "locked change, required for remaining == 0", + changeAmount: int64(expectedFees - 1), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: []uint64{0, 0, 0, 0}, + newFeeRate: 50, + numUtxosUsed: 2, + expectChange: true, + expectAccelerationEstimateErr: true, + expectAccelerateOrderErr: true, + expectPreAccelerateErr: true, + lockChange: true, + utxos: []utxo{ + { + confs: 1, + amount: 2e6, + }, + { + confs: 1, + amount: 1e6, + }, + }, + }, + } + + for _, test := range tests { + node.listUnspent = []*ListUnspentResult{} + swapCoins, accelerations, changeCoin, txs := getAccelerationParams(test.changeAmount, !test.changeNotInUnspent, test.addPreviousAcceleration, test.fees, node) + expectedFees := (sumTxSizes(txs, test.confs)+calculateChangeTxSize(test.expectChange, segwit, 1+test.numUtxosUsed))*test.newFeeRate - sumFees(test.fees, test.confs) + if !test.expectChange { + expectedFees = uint64(test.changeAmount) + } + if test.scrambleSwapCoins { + temp := swapCoins[0] + swapCoins[0] = swapCoins[2] + swapCoins[2] = swapCoins[1] + swapCoins[1] = temp + } + if test.lockChange { + changeTxID, changeVout, _ := decodeCoinID(changeCoin) + node.listLockUnspent = []*RPCOutpoint{ + { + TxID: changeTxID.String(), + Vout: changeVout, + }, + } + } + for _, utxo := range test.utxos { + addUTXOToNode(utxo.confs, segwit, utxo.amount, node) + } + + loadTxsIntoNode(txs, test.fees, test.confs, node, t) + + testAccelerateOrder := func() { + change, txID, err := wallet.AccelerateOrder(swapCoins, accelerations, changeCoin, test.requiredForRemainingSwaps, test.newFeeRate) + if test.expectAccelerateOrderErr { + if err == nil { + t.Fatalf("%s: expected AccelerateOrder error but did not get", test.name) + } + return + } + if err != nil { + t.Fatalf("%s: unexpected error: %v", test.name, err) + } + + in, out := totalInputOutput(node.sentRawTx) + lastTxFee := in - out + if expectedFees != lastTxFee { + t.Fatalf("%s: expected fee to be %d but got %d", test.name, expectedFees, lastTxFee) + } + + if node.sentRawTx.TxHash().String() != txID { + t.Fatalf("%s: expected tx id %s, but got %s", test.name, node.sentRawTx.TxHash().String(), txID) + } + + if test.expectChange { + if out == 0 { + t.Fatalf("%s: expected change but did not get", test.name) + } + if change == nil { + t.Fatalf("%s: expected change, but got nil", test.name) + } + + changeScript := node.sentRawTx.TxOut[0].PkScript + + changeAddr, err := btcutil.DecodeAddress(node.changeAddr, &chaincfg.MainNetParams) + if err != nil { + t.Fatalf("%s: unexpected error: %v", test.name, err) + } + expectedChangeScript, err := txscript.PayToAddrScript(changeAddr) + if err != nil { + t.Fatalf("%s: unexpected error: %v", test.name, err) + } + + if !bytes.Equal(changeScript, expectedChangeScript) { + t.Fatalf("%s: expected change script != actual", test.name) + } + + changeTxHash, changeVout, err := decodeCoinID(change.ID()) + if err != nil { + t.Fatalf("%s: unexpected error: %v", test.name, err) + } + if *changeTxHash != node.sentRawTx.TxHash() { + t.Fatalf("%s: change tx hash %x != expected: %x", test.name, changeTxHash, node.sentRawTx.TxHash()) + } + if changeVout != 0 { + t.Fatalf("%s: change vout %v != expected: 0", test.name, changeVout) + } + + var changeLocked bool + for _, coin := range node.lockedCoins { + if changeVout == coin.Vout && changeTxHash.String() == coin.TxID { + changeLocked = true + } + } + if changeLocked != test.expectChangeLocked { + t.Fatalf("%s: expected change locked = %v, but was %v", test.name, test.expectChangeLocked, changeLocked) + } + } else { + if out > 0 { + t.Fatalf("%s: not expecting change but got %v", test.name, out) + } + if change != nil { + t.Fatalf("%s: not expecting change but accelerate returned: %+v", test.name, change) + } + } + + if test.requiredForRemainingSwaps > out { + t.Fatalf("%s: %d needed of remaining swaps, but output was only %d", test.name, test.requiredForRemainingSwaps, out) + } + } + testAccelerateOrder() + + testAccelerationEstimate := func() { + estimate, err := wallet.AccelerationEstimate(swapCoins, accelerations, changeCoin, test.requiredForRemainingSwaps, test.newFeeRate) + if test.expectAccelerationEstimateErr { + if err == nil { + t.Fatalf("%s: expected AccelerationEstimate error but did not get", test.name) + } + return + } + if err != nil { + t.Fatalf("%s: unexpected error: %v", test.name, err) + } + if estimate != expectedFees { + t.Fatalf("%s: estimate %v != expected fees %v", test.name, estimate, expectedFees) + } + } + testAccelerationEstimate() + + testPreAccelerate := func() { + currentRate, suggestedRange, err := wallet.PreAccelerate(swapCoins, accelerations, changeCoin, test.requiredForRemainingSwaps, test.suggestedFeeRate) + if test.expectPreAccelerateErr { + if err == nil { + t.Fatalf("%s: expected PreAccelerate error but did not get", test.name) + } + return + } + if err != nil { + t.Fatalf("%s: unexpected error: %v", test.name, err) + } + + var totalSize, totalFee uint64 + for i, tx := range txs { + if test.confs[i] == 0 { + totalSize += dexbtc.MsgTxVBytes(tx) + totalFee += toSatoshi(test.fees[i]) + } + } + + expectedRate := totalFee / totalSize + if expectedRate != currentRate { + t.Fatalf("%s: expected current rate %v != actual %v", test.name, expectedRate, currentRate) + } + + totalFeePossible := totalFee + var numConfirmedUTXO int + for _, utxo := range test.utxos { + if utxo.confs > 0 { + totalFeePossible += utxo.amount + numConfirmedUTXO++ + } + } + + totalSize += calculateChangeTxSize(test.expectChange, segwit, 1+numConfirmedUTXO) + + totalFeePossible += uint64(test.changeAmount) + totalFeePossible -= test.requiredForRemainingSwaps + + maxRatePossible := totalFeePossible / totalSize + + expectedRangeHigh := expectedRate * 5 + if test.suggestedFeeRate > expectedRate { + expectedRangeHigh = test.suggestedFeeRate * 5 + } + if maxRatePossible < expectedRangeHigh { + expectedRangeHigh = maxRatePossible + } + + expectedRangeLowX := float64(expectedRate+1) / float64(expectedRate) + if suggestedRange.Start.X != expectedRangeLowX { + t.Fatalf("%s: start of range should be %v on X, got: %v", test.name, expectedRangeLowX, suggestedRange.Start.X) + } + + if suggestedRange.Start.Y != float64(expectedRate+1) { + t.Fatalf("%s: expected start of range on Y to be: %v, but got %v", test.name, float64(expectedRate), suggestedRange.Start.Y) + } + + if suggestedRange.End.Y != float64(expectedRangeHigh) { + t.Fatalf("%s: expected end of range on Y to be: %v, but got %v", test.name, float64(expectedRangeHigh), suggestedRange.End.Y) + } + + expectedRangeHighX := float64(expectedRangeHigh) / float64(expectedRate) + if suggestedRange.End.X != expectedRangeHighX { + t.Fatalf("%s: expected end of range on X to be: %v, but got %v", test.name, float64(expectedRate), suggestedRange.End.X) + } + } + testPreAccelerate() + } +} diff --git a/client/asset/btc/rpcclient.go b/client/asset/btc/rpcclient.go index 8e827b17b2..d106875b83 100644 --- a/client/asset/btc/rpcclient.go +++ b/client/asset/btc/rpcclient.go @@ -704,7 +704,7 @@ func (wc *rpcClient) call(method string, args anylist, thing interface{}) error } // serializeMsgTx serializes the wire.MsgTx. -func serializeMsgTx(msgTx *wire.MsgTx) ([]byte, error) { +func serializeMsgTx(msgTx *wire.MsgTx) (dex.Bytes, error) { buf := bytes.NewBuffer(make([]byte, 0, msgTx.SerializeSize())) err := msgTx.Serialize(buf) if err != nil { diff --git a/client/asset/btc/spv.go b/client/asset/btc/spv.go index 38442f0f44..a477bea67b 100644 --- a/client/asset/btc/spv.go +++ b/client/asset/btc/spv.go @@ -1799,16 +1799,14 @@ func (w *spvWallet) getTransaction(txHash *chainhash.Hash) (*GetTransactionResul // TODO: The serialized transaction is already in the DB, so // reserializing can be avoided here. - var txBuf bytes.Buffer - txBuf.Grow(details.MsgTx.SerializeSize()) - err = details.MsgTx.Serialize(&txBuf) + txBuf, err := serializeMsgTx(&details.MsgTx) if err != nil { return nil, err } ret := &GetTransactionResult{ TxID: txHash.String(), - Hex: txBuf.Bytes(), // 'Hex' field name is a lie, kinda + Hex: txBuf, // 'Hex' field name is a lie, kinda Time: uint64(details.Received.Unix()), TimeReceived: uint64(details.Received.Unix()), } @@ -1816,6 +1814,7 @@ func (w *spvWallet) getTransaction(txHash *chainhash.Hash) (*GetTransactionResul if details.Block.Height != -1 { ret.BlockHash = details.Block.Hash.String() ret.BlockTime = uint64(details.Block.Time.Unix()) + ret.BlockHeight = uint64(details.Block.Height) ret.Confirmations = uint64(confirms(details.Block.Height, syncBlock.Height)) } diff --git a/client/asset/btc/spv_test.go b/client/asset/btc/spv_test.go index 44130e9cfd..45541e1cb7 100644 --- a/client/asset/btc/spv_test.go +++ b/client/asset/btc/spv_test.go @@ -204,27 +204,51 @@ func (c *tBtcWallet) walletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetail if c.getTransactionErr != nil { return nil, c.getTransactionErr } - if c.testData.getTransaction == nil { + var txData *GetTransactionResult + if c.getTransactionMap != nil { + if txData = c.getTransactionMap["any"]; txData == nil { + txData = c.getTransactionMap[txHash.String()] + } + } + if txData == nil { return nil, WalletTransactionNotFound } - txData := c.testData.getTransaction tx, _ := msgTxFromBytes(txData.Hex) - blockHash, _ := chainhash.NewHashFromStr(txData.BlockHash) blk := c.getBlock(txData.BlockHash) + var blockHeight int32 + if blk != nil { + blockHeight = int32(blk.height) + } else { + blockHeight = -1 + } credits := make([]wtxmgr.CreditRecord, 0, len(tx.TxIn)) - for i := range tx.TxIn { + debits := make([]wtxmgr.DebitRecord, 0, len(tx.TxIn)) + for i, in := range tx.TxIn { credits = append(credits, wtxmgr.CreditRecord{ // Amount:, Index: uint32(i), Spent: c.walletTxSpent, // Change: , }) - } + var debitAmount int64 + // The sources of transaction inputs all need to be added to getTransactionMap + // in order to get accurate Fees and Amounts when calling GetWalletTransaction + // when using the SPV wallet. + if gtr := c.getTransactionMap[in.PreviousOutPoint.Hash.String()]; gtr != nil { + tx, _ := msgTxFromBytes(gtr.Hex) + debitAmount = tx.TxOut[in.PreviousOutPoint.Index].Value + } + + debits = append(debits, wtxmgr.DebitRecord{ + Amount: btcutil.Amount(debitAmount), + }) + + } return &wtxmgr.TxDetails{ TxRecord: wtxmgr.TxRecord{ MsgTx: *tx, @@ -232,10 +256,11 @@ func (c *tBtcWallet) walletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetail Block: wtxmgr.BlockMeta{ Block: wtxmgr.Block{ Hash: *blockHash, - Height: int32(blk.height), + Height: blockHeight, }, }, Credits: credits, + Debits: debits, }, nil } @@ -243,7 +268,16 @@ func (c *tBtcWallet) getTransaction(txHash *chainhash.Hash) (*GetTransactionResu if c.getTransactionErr != nil { return nil, c.getTransactionErr } - return c.testData.getTransaction, nil + var txData *GetTransactionResult + if c.getTransactionMap != nil { + if txData = c.getTransactionMap["any"]; txData == nil { + txData = c.getTransactionMap[txHash.String()] + } + } + if txData == nil { + return nil, WalletTransactionNotFound + } + return txData, nil } func (c *tBtcWallet) syncedTo() waddrmgr.BlockStamp { @@ -419,14 +453,15 @@ func TestSwapConfirmations(t *testing.T) { node.confs = 10 node.confsSpent = true txB, _ := serializeMsgTx(swapTx) - node.getTransaction = &GetTransactionResult{ - BlockHash: swapBlockHash.String(), - BlockIndex: swapHeight, - Hex: txB, - } + node.getTransactionMap = map[string]*GetTransactionResult{ + "any": &GetTransactionResult{ + BlockHash: swapBlockHash.String(), + BlockIndex: swapHeight, + Hex: txB, + }} node.walletTxSpent = true checkSuccess("confirmations", swapConfs, true) - node.getTransaction = nil + node.getTransactionMap = nil node.walletTxSpent = false node.confsErr = WalletTransactionNotFound @@ -555,10 +590,10 @@ func TestGetTxOut(t *testing.T) { // Wallet transaction found node.getTransactionErr = nil - node.getTransaction = &GetTransactionResult{ + node.getTransactionMap = map[string]*GetTransactionResult{"any": &GetTransactionResult{ BlockHash: blockHash.String(), Hex: txB, - } + }} _, confs, err := spv.getTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight)) if err != nil { @@ -570,7 +605,7 @@ func TestGetTxOut(t *testing.T) { // No wallet transaction, but we have a spend recorded. node.getTransactionErr = WalletTransactionNotFound - node.getTransaction = nil + node.getTransactionMap = nil node.checkpoints[outPt] = &scanCheckpoint{res: &filterScanResult{ blockHash: blockHash, spend: &spendingInput{}, @@ -633,7 +668,7 @@ func TestSendWithSubtract(t *testing.T) { const availableFunds = 5e8 const feeRate = 100 - const inputSize = dexbtc.RedeemP2WPKHInputSize + ((dexbtc.RedeemP2WPKHInputWitnessWeight + 2 + 3) / 4) + const inputSize = dexbtc.RedeemP2PWKHInputTotalSize const feesWithChange = (dexbtc.MinimumTxOverhead + 2*dexbtc.P2WPKHOutputSize + inputSize) * feeRate const feesWithoutChange = (dexbtc.MinimumTxOverhead + dexbtc.P2WPKHOutputSize + inputSize) * feeRate diff --git a/client/asset/btc/wallettypes.go b/client/asset/btc/wallettypes.go index 38a74c940a..6c56481e19 100644 --- a/client/asset/btc/wallettypes.go +++ b/client/asset/btc/wallettypes.go @@ -63,6 +63,7 @@ type GetTransactionResult struct { BlockHash string `json:"blockhash"` BlockIndex int64 `json:"blockindex"` BlockTime uint64 `json:"blocktime"` + BlockHeight uint64 `json:"blockheight"` TxID string `json:"txid"` Time uint64 `json:"time"` TimeReceived uint64 `json:"timereceived"` diff --git a/client/asset/interface.go b/client/asset/interface.go index f5bbddb8c0..ab083c1218 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -19,6 +19,7 @@ const ( WalletTraitNewAddresser // The Wallet can generate new addresses on demand with NewAddress. WalletTraitLogFiler // The Wallet allows for downloading of a log file. WalletTraitFeeRater // Wallet can provide a fee rate for non-critical transactions + WalletTraitAccelerator // This wallet can accelerate transactions using the CPFP technique ) // IsRescanner tests if the WalletTrait has the WalletTraitRescanner bit set. @@ -45,6 +46,12 @@ func (wt WalletTrait) IsFeeRater() bool { return wt&WalletTraitFeeRater != 0 } +// IsAccelerator tests if the WalletTrait has the WalletTraitAccelerator bit set, +// which indicates the presence of a Accelerate method. +func (wt WalletTrait) IsAccelerator() bool { + return wt&WalletTraitAccelerator != 0 +} + // DetermineWalletTraits returns the WalletTrait bitset for the provided Wallet. func DetermineWalletTraits(w Wallet) (t WalletTrait) { if _, is := w.(Rescanner); is { @@ -59,6 +66,9 @@ func DetermineWalletTraits(w Wallet) (t WalletTrait) { if _, is := w.(FeeRater); is { t |= WalletTraitFeeRater } + if _, is := w.(Accelerator); is { + t |= WalletTraitAccelerator + } return t } @@ -360,6 +370,28 @@ type FeeRater interface { FeeRate() uint64 } +// Accelerator is implemented by wallets which support acceleration of the +// mining of swap transactions. +type Accelerator interface { + // AccelerateOrder uses the Child-Pays-For-Parent technique to accelerate a + // chain of swap transactions and previous accelerations. It broadcasts a new + // transaction with a fee high enough so that the average fee of all the + // unconfirmed transactions in the chain and the new transaction will have + // an average fee rate of newFeeRate. requiredForRemainingSwaps is passed + // in to ensure that the new change coin will have enough funds to initiate + // the additional swaps that will be required to complete the order. + AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (Coin, string, error) + // AccelerationEstimate takes the same parameters as AccelerateOrder, but + // instead of broadcasting the acceleration transaction, it just returns + // the amount of funds that will need to be spent in order to increase the + // average fee rate to the desired amount. + AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) + // PreAccelerate returns the current average fee rate of the unmined swap initiation + // and acceleration transactions, and also returns a suggested range that the + // fee rate should be increased to in order to expedite mining. + PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange XYRange, err error) +} + // TokenMaster is implemented by assets which support degenerate tokens. type TokenMaster interface { // CreateTokenWallet creates a wallet for the specified token asset. The diff --git a/client/core/core.go b/client/core/core.go index a724735aea..680a92cc3c 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -3494,6 +3494,7 @@ func (c *Core) Order(oidB dex.Bytes) (*Order, error) { if err != nil { return nil, fmt.Errorf("error retrieving order %s: %w", oid, err) } + return c.coreOrderFromMetaOrder(mOrd) } @@ -7128,3 +7129,139 @@ func (c *Core) DeleteArchivedRecords(olderThan *time.Time, matchesFile, ordersFi } return nil } + +// AccelerateOrder will use the Child-Pays-For-Parent technique to accelerate +// the swap transactions in an order. +func (c *Core) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (string, error) { + _, err := c.encryptionKey(pw) + if err != nil { + return "", fmt.Errorf("Accelerate password error: %w", err) + } + + tracker, swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := c.orderAccelerationParameters(oidB) + if err != nil { + return "", err + } + + newChangeCoin, txID, err := + tracker.wallets.fromWallet.AccelerateOrder(swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, newFeeRate) + if err != nil { + return "", err + } + + tracker.metaData.ChangeCoin = order.CoinID(newChangeCoin.ID()) + tracker.metaData.AccelerationCoins = append(tracker.metaData.AccelerationCoins, tracker.metaData.ChangeCoin) + tracker.coins[newChangeCoin.ID().String()] = newChangeCoin + err = tracker.db.UpdateOrder(tracker.metaOrder()) + if err != nil { + c.log.Errorf("AccelerateOrder: failed to update order in database: %v", err) + } + + return txID, nil +} + +// AccelerationEstimate returns the amount of funds that would be needed to +// accelerate the swap transactions in an order to a desired fee rate. +func (c *Core) AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, error) { + tracker, swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, err := c.orderAccelerationParameters(oidB) + if err != nil { + return 0, err + } + + accelerationFee, err := tracker.wallets.fromWallet.AccelerationEstimate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) + if err != nil { + return 0, err + } + + return accelerationFee, nil +} + +// PreAccelerateOrder returns information the user can use to decide how much +// to accelerate stuck swap transactions in an order. +func (c *Core) PreAccelerateOrder(oidB dex.Bytes) (*PreAccelerate, error) { + tracker, swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := c.orderAccelerationParameters(oidB) + if err != nil { + return nil, err + } + + feeSuggestion := c.feeSuggestionAny(tracker.fromAssetID) + + currentRate, suggestedRange, err := + tracker.wallets.fromWallet.PreAccelerate(swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, feeSuggestion) + if err != nil { + return nil, err + } + + return &PreAccelerate{ + SwapRate: currentRate, + SuggestedRate: feeSuggestion, + SuggestedRange: suggestedRange, + }, nil +} + +// orderAccelerationParameters takes an order id, and returns the parameters +// needed to accelerate the swap transactions in that order. +func (c *Core) orderAccelerationParameters(oidB dex.Bytes) (tracker *trackedTrade, swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps uint64, err error) { + makeError := func(err error) (*trackedTrade, []dex.Bytes, []dex.Bytes, dex.Bytes, uint64, error) { + return nil, nil, nil, nil, 0, err + } + + if len(oidB) != order.OrderIDSize { + return makeError(fmt.Errorf("wrong order ID length. wanted %d, got %d", order.OrderIDSize, len(oidB))) + } + var oid order.OrderID + copy(oid[:], oidB) + + for _, dc := range c.dexConnections() { + tracker, _, _ = dc.findOrder(oid) + if tracker != nil { + mktID := marketName(tracker.Base(), tracker.Quote()) + mkt := dc.marketConfig(mktID) + if mkt == nil { + return makeError(fmt.Errorf("could not find market: %v", mktID)) + } + lotSize := mkt.LotSize + fromAsset := dc.assets[tracker.fromAssetID] + if fromAsset == nil { + return makeError(fmt.Errorf("could not find asset with id: %v", tracker.fromAssetID)) + } + swapSize := fromAsset.SwapSize + lotsRemaining := tracker.Trade().Remaining() / lotSize + requiredForRemainingSwaps = lotsRemaining * swapSize * tracker.metaData.MaxFeeRate + } + } + if tracker == nil { + return makeError(fmt.Errorf("could not find active order with id: %s", oid)) + } + + tracker.mtx.Lock() + defer tracker.mtx.Unlock() + + swapCoins = make([]dex.Bytes, 0, len(tracker.matches)) + for _, match := range tracker.matches { + if match.Status < order.MakerSwapCast { + continue + } + var swapCoinID order.CoinID + if match.Side == order.Maker { + swapCoinID = match.MetaData.Proof.MakerSwap + } else { + if match.Status < order.TakerSwapCast { + continue + } + swapCoinID = match.MetaData.Proof.TakerSwap + } + swapCoins = append(swapCoins, dex.Bytes(swapCoinID)) + } + + accelerationCoins = make([]dex.Bytes, 0, len(tracker.metaData.AccelerationCoins)) + for _, coin := range tracker.metaData.AccelerationCoins { + accelerationCoins = append(accelerationCoins, dex.Bytes(coin)) + } + + if tracker.metaData.ChangeCoin == nil { + return makeError(fmt.Errorf("order does not have change which can be accelerated")) + } + + return tracker, swapCoins, accelerationCoins, dex.Bytes(tracker.metaData.ChangeCoin), requiredForRemainingSwaps, nil +} diff --git a/client/core/core_test.go b/client/core/core_test.go index ba423c0825..c27e884a64 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -638,8 +638,21 @@ type TXCWallet struct { sigs []dex.Bytes contractExpired bool contractLockTime time.Time + accelerationParams *struct { + swapCoins []dex.Bytes + accelerationCoins []dex.Bytes + changeCoin dex.Bytes + feeSuggestion uint64 + newFeeRate uint64 + requiredForRemainingSwaps uint64 + } + newAccelerationTxID string + newChangeCoinID dex.Bytes + accelerateOrderErr error } +var _ asset.Accelerator = (*TXCWallet)(nil) + func newTWallet(assetID uint32) (*xcWallet, *TXCWallet) { w := &TXCWallet{ changeCoin: &tCoin{id: encode.RandomBytes(36)}, @@ -859,6 +872,36 @@ func (w *TXCWallet) RegFeeConfirmations(ctx context.Context, coinID dex.Bytes) ( return w.tConfirmations(ctx, coinID) } +func (w *TXCWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { + if w.accelerateOrderErr != nil { + return nil, "", w.accelerateOrderErr + } + + w.accelerationParams = &struct { + swapCoins []dex.Bytes + accelerationCoins []dex.Bytes + changeCoin dex.Bytes + feeSuggestion uint64 + newFeeRate uint64 + requiredForRemainingSwaps uint64 + }{ + swapCoins: swapCoins, + accelerationCoins: accelerationCoins, + changeCoin: changeCoin, + requiredForRemainingSwaps: requiredForRemainingSwaps, + newFeeRate: newFeeRate, + } + return &tCoin{id: w.newChangeCoinID}, w.newAccelerationTxID, nil +} + +func (w *TXCWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange asset.XYRange, err error) { + return 0, asset.XYRange{}, nil +} + +func (w *TXCWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, error) { + return 0, nil +} + type TAccountLocker struct { *TXCWallet reserveNRedemptions uint64 @@ -7042,6 +7085,147 @@ func TestPreimageSync(t *testing.T) { } } +func TestAccelerateOrder(t *testing.T) { + rig := newTestRig() + defer rig.shutdown() + tCore := rig.core + dc := rig.dc + mkt := dc.marketConfig(tDcrBtcMktName) + + dcrWallet, _ := newTWallet(tUTXOAssetA.ID) + tCore.wallets[tUTXOAssetA.ID] = dcrWallet + btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) + tCore.wallets[tUTXOAssetB.ID] = btcWallet + walletSet, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, false) + + qty := 3 * dcrBtcLotSize + + lo, dbOrder, preImg, addr := makeLimitOrder(dc, false, qty, dcrBtcRateStep*10) + dbOrder.MetaData.Status = order.OrderStatusExecuted // so there is no order_status request for this + oid := lo.ID() + trade := newTrackedTrade(dbOrder, preImg, dc, mkt.EpochLen, rig.core.lockTimeTaker, rig.core.lockTimeMaker, + rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails, nil, 0, 0) + + dc.trades[trade.ID()] = trade + + match1ID := ordertest.RandomMatchID() + match1 := &matchTracker{ + MetaMatch: db.MetaMatch{ + MetaData: &db.MatchMetaData{ + Proof: db.MatchProof{ + MakerSwap: encode.RandomBytes(32), + TakerSwap: encode.RandomBytes(32), + }, + }, + UserMatch: &order.UserMatch{ + MatchID: match1ID, + Address: addr, + Side: order.Maker, + Status: order.TakerSwapCast, + }, + }, + } + trade.matches[match1ID] = match1 + + match2ID := ordertest.RandomMatchID() + match2 := &matchTracker{ + MetaMatch: db.MetaMatch{ + MetaData: &db.MatchMetaData{ + Proof: db.MatchProof{ + MakerSwap: encode.RandomBytes(32), + TakerSwap: encode.RandomBytes(32), + }, + }, + UserMatch: &order.UserMatch{ + MatchID: match2ID, + Address: addr, + Side: order.Taker, + Status: order.TakerSwapCast, + }, + }, + } + trade.matches[match2ID] = match2 + + trade.metaData.ChangeCoin = encode.RandomBytes(32) + originalChangeCoin := trade.metaData.ChangeCoin + trade.metaData.AccelerationCoins = []order.CoinID{encode.RandomBytes(32)} + tBtcWallet.newChangeCoinID = encode.RandomBytes(32) + tBtcWallet.newAccelerationTxID = hex.EncodeToString(encode.RandomBytes(32)) + + txID, err := tCore.AccelerateOrder(tPW, oid.Bytes(), 50) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(tBtcWallet.accelerationParams.swapCoins) != 2 { + t.Fatalf("expected 2 swap coins but got %v", len(tBtcWallet.accelerationParams.swapCoins)) + } + expectedSwapCoins := []order.CoinID{match1.MetaMatch.MetaData.Proof.MakerSwap, match2.MetaMatch.MetaData.Proof.TakerSwap} + swapCoins := tBtcWallet.accelerationParams.swapCoins + if !((bytes.Equal(swapCoins[0], expectedSwapCoins[0]) && bytes.Equal(swapCoins[1], expectedSwapCoins[1])) || + (bytes.Equal(swapCoins[0], expectedSwapCoins[1]) && bytes.Equal(swapCoins[1], expectedSwapCoins[0]))) { + t.Fatalf("swap coins not same as expected") + } + + if !bytes.Equal(tBtcWallet.accelerationParams.changeCoin, originalChangeCoin) { + t.Fatalf("change coin not same as expected %x - %x", tBtcWallet.accelerationParams.changeCoin, trade.metaData.ChangeCoin) + } + + accelerationCoins := tBtcWallet.accelerationParams.accelerationCoins + if len(accelerationCoins) != 1 { + t.Fatalf("expected 1 acceleration tx but got %v", len(accelerationCoins)) + } + if !bytes.Equal(accelerationCoins[0], trade.metaData.AccelerationCoins[0]) { + t.Fatalf("acceleration tx id not same as expected") + } + + if !bytes.Equal(trade.metaData.ChangeCoin, tBtcWallet.newChangeCoinID) { + t.Fatalf("change coin on trade was not updated to return value from AccelerateOrder") + } + if !bytes.Equal(trade.metaData.AccelerationCoins[len(trade.metaData.AccelerationCoins)-1], tBtcWallet.newChangeCoinID) { + t.Fatalf("new acceleration transaction id was not added to the trade") + } + if txID != tBtcWallet.newAccelerationTxID { + t.Fatalf("new acceleration transaction id was not returned from AccelerateOrder") + } + + var inCoinsList bool + for _, coin := range trade.coins { + if bytes.Equal(coin.ID(), tBtcWallet.newChangeCoinID) { + inCoinsList = true + } + } + if !inCoinsList { + t.Fatalf("new change coin must be added to the trade.coins slice") + } + + // Ensure error with order id with incorrect length + _, err = tCore.AccelerateOrder(tPW, encode.RandomBytes(31), 50) + if err == nil { + t.Fatalf("expected error but did not get") + } + + // Ensure error with non active order id + _, err = tCore.AccelerateOrder(tPW, encode.RandomBytes(order.OrderIDSize), 50) + if err == nil { + t.Fatalf("expected error but did not get") + } + + // Ensure error when wallet method returns an error + tBtcWallet.accelerateOrderErr = errors.New("") + _, err = tCore.AccelerateOrder(tPW, oid.Bytes(), 50) + if err == nil { + t.Fatalf("expected error but did not get") + } + + // Ensure error when change coin is not set + trade.metaData.ChangeCoin = nil + _, err = tCore.AccelerateOrder(tPW, oid.Bytes(), 50) + if err == nil { + t.Fatalf("expected error but did not get") + } +} + func TestMatchStatusResolution(t *testing.T) { rig := newTestRig() defer rig.shutdown() diff --git a/client/core/types.go b/client/core/types.go index 80236c5d56..9f5f3829a4 100644 --- a/client/core/types.go +++ b/client/core/types.go @@ -281,30 +281,31 @@ func matchFromMetaMatchWithConfs(ord order.Order, metaMatch *db.MetaMatch, swapC // Order is core's general type for an order. An order may be a market, limit, // or cancel order. Some fields are only relevant to particular order types. type Order struct { - Host string `json:"host"` - BaseID uint32 `json:"baseID"` - BaseSymbol string `json:"baseSymbol"` - QuoteID uint32 `json:"quoteID"` - QuoteSymbol string `json:"quoteSymbol"` - MarketID string `json:"market"` - Type order.OrderType `json:"type"` - ID dex.Bytes `json:"id"` - Stamp uint64 `json:"stamp"` - Sig dex.Bytes `json:"sig"` - Status order.OrderStatus `json:"status"` - Epoch uint64 `json:"epoch"` - Qty uint64 `json:"qty"` - Sell bool `json:"sell"` - Filled uint64 `json:"filled"` - Matches []*Match `json:"matches"` - Cancelling bool `json:"cancelling"` - Canceled bool `json:"canceled"` - FeesPaid *FeeBreakdown `json:"feesPaid"` - FundingCoins []*Coin `json:"fundingCoins"` - LockedAmt uint64 `json:"lockedamt"` - Rate uint64 `json:"rate"` // limit only - TimeInForce order.TimeInForce `json:"tif"` // limit only - TargetOrderID dex.Bytes `json:"targetOrderID"` // cancel only + Host string `json:"host"` + BaseID uint32 `json:"baseID"` + BaseSymbol string `json:"baseSymbol"` + QuoteID uint32 `json:"quoteID"` + QuoteSymbol string `json:"quoteSymbol"` + MarketID string `json:"market"` + Type order.OrderType `json:"type"` + ID dex.Bytes `json:"id"` + Stamp uint64 `json:"stamp"` + Sig dex.Bytes `json:"sig"` + Status order.OrderStatus `json:"status"` + Epoch uint64 `json:"epoch"` + Qty uint64 `json:"qty"` + Sell bool `json:"sell"` + Filled uint64 `json:"filled"` + Matches []*Match `json:"matches"` + Cancelling bool `json:"cancelling"` + Canceled bool `json:"canceled"` + FeesPaid *FeeBreakdown `json:"feesPaid"` + FundingCoins []*Coin `json:"fundingCoins"` + LockedAmt uint64 `json:"lockedamt"` + AccelerationCoins []*Coin `json:"accelerationCoins"` + Rate uint64 `json:"rate"` // limit only + TimeInForce order.TimeInForce `json:"tif"` // limit only + TargetOrderID dex.Bytes `json:"targetOrderID"` // cancel only } // FeeBreakdown is categorized fee information. @@ -362,6 +363,11 @@ func coreOrderFromTrade(ord order.Order, metaData *db.OrderMetaData) *Order { fundingCoins = append(fundingCoins, NewCoin(fromID, trade.Coins[i])) } + accelerationCoins := make([]*Coin, 0, len(metaData.AccelerationCoins)) + for _, coinID := range metaData.AccelerationCoins { + accelerationCoins = append(accelerationCoins, NewCoin(fromID, coinID)) + } + corder := &Order{ Host: metaData.Host, BaseID: baseID, @@ -385,7 +391,8 @@ func coreOrderFromTrade(ord order.Order, metaData *db.OrderMetaData) *Order { Swap: metaData.SwapFeesPaid, Redemption: metaData.RedemptionFeesPaid, }, - FundingCoins: fundingCoins, + FundingCoins: fundingCoins, + AccelerationCoins: accelerationCoins, } return corder @@ -903,3 +910,11 @@ type OrderEstimate struct { Swap *asset.PreSwap `json:"swap"` Redeem *asset.PreRedeem `json:"redeem"` } + +// PreAccelerate gives information that the user can use to decide on +// how much to accelerate stuck swap transactions in an order. +type PreAccelerate struct { + SwapRate uint64 `json:"swapRate"` + SuggestedRate uint64 `json:"suggestedRate"` + SuggestedRange asset.XYRange `json:"suggestedRange"` +} diff --git a/client/core/wallet.go b/client/core/wallet.go index cef27f6965..ff5d215b39 100644 --- a/client/core/wallet.go +++ b/client/core/wallet.go @@ -302,6 +302,38 @@ func (w *xcWallet) LogFilePath() (string, error) { return logFiler.LogFilePath(), nil } +// AccelerateOrder uses the Child-Pays-For-Parent technique to accelerate an +// order if the wallet is an Accelerator. +func (w *xcWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { + accelerator, ok := w.Wallet.(asset.Accelerator) + if !ok { + return nil, "", errors.New("wallet does not support acceleration") + } + return accelerator.AccelerateOrder(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) +} + +// AccelerationEstimate estimates the cost to accelerate an order if the wallet +// is an Accelerator. +func (w *xcWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, error) { + accelerator, ok := w.Wallet.(asset.Accelerator) + if !ok { + return 0, errors.New("wallet does not support acceleration") + } + + return accelerator.AccelerationEstimate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion) +} + +// PreAccelerate gives the user information about accelerating an order if the +// wallet is an Accelerator. +func (w *xcWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange asset.XYRange, err error) { + accelerator, ok := w.Wallet.(asset.Accelerator) + if !ok { + return 0, asset.XYRange{}, errors.New("wallet does not support acceleration") + } + + return accelerator.PreAccelerate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion) +} + // SwapConfirmations calls (asset.Wallet).SwapConfirmations with a timeout // Context. If the coin cannot be located, an asset.CoinNotFoundError is // returned. If the coin is located, but recognized as spent, no error is diff --git a/client/db/bolt/db.go b/client/db/bolt/db.go index acf6e67e8a..0d640afd49 100644 --- a/client/db/bolt/db.go +++ b/client/db/bolt/db.go @@ -87,6 +87,7 @@ var ( maxFeeRateKey = []byte("maxFeeRate") redeemMaxFeeRateKey = []byte("redeemMaxFeeRate") redemptionFeesKey = []byte("redeemFees") + accelerationsKey = []byte("accelerations") typeKey = []byte("type") credentialsBucket = []byte("credentials") seedGenTimeKey = []byte("seedGenTime") @@ -674,6 +675,14 @@ func (db *BoltDB) UpdateOrder(m *dexdb.MetaOrder) error { linkedB = md.LinkedOrder[:] } + var accelerationsB encode.BuildyBytes + if len(md.AccelerationCoins) > 0 { + accelerationsB = encode.BuildyBytes{0} + for _, acceleration := range md.AccelerationCoins { + accelerationsB = accelerationsB.AddData(acceleration) + } + } + return newBucketPutter(oBkt). put(baseKey, uint32Bytes(ord.Base())). put(quoteKey, uint32Bytes(ord.Quote())). @@ -690,6 +699,7 @@ func (db *BoltDB) UpdateOrder(m *dexdb.MetaOrder) error { put(redeemMaxFeeRateKey, uint64Bytes(md.RedeemMaxFeeRate)). put(redemptionFeesKey, uint64Bytes(md.RedemptionFeesPaid)). put(optionsKey, config.Data(md.Options)). + put(accelerationsKey, accelerationsB). err() }) } @@ -1002,6 +1012,18 @@ func decodeOrderBucket(oid []byte, oBkt *bbolt.Bucket) (*dexdb.MetaOrder, error) return nil, fmt.Errorf("unable to decode order options") } + var accelerationCoinIDs []order.CoinID + accelerationsB := oBkt.Get(accelerationsKey) + if len(accelerationsB) > 0 { + _, coinIDs, err := encode.DecodeBlob(accelerationsB) + if err != nil { + return nil, fmt.Errorf("unable to decode accelerations") + } + for _, coinID := range coinIDs { + accelerationCoinIDs = append(accelerationCoinIDs, order.CoinID(coinID)) + } + } + return &dexdb.MetaOrder{ MetaData: &dexdb.OrderMetaData{ Proof: *proof, @@ -1018,6 +1040,7 @@ func decodeOrderBucket(oid []byte, oBkt *bbolt.Bucket) (*dexdb.MetaOrder, error) Options: options, RedemptionReserves: redemptionReserves, RefundReserves: refundReserves, + AccelerationCoins: accelerationCoinIDs, }, Order: ord, }, nil @@ -1080,6 +1103,14 @@ func (db *BoltDB) UpdateOrderMetaData(oid order.OrderID, md *db.OrderMetaData) e linkedB = md.LinkedOrder[:] } + var accelerationsB encode.BuildyBytes + if len(md.AccelerationCoins) > 0 { + accelerationsB = encode.BuildyBytes{0} + for _, acceleration := range md.AccelerationCoins { + accelerationsB = accelerationsB.AddData(acceleration) + } + } + return newBucketPutter(oBkt). put(statusKey, uint16Bytes(uint16(md.Status))). put(updateTimeKey, uint64Bytes(timeNow())). @@ -1094,6 +1125,7 @@ func (db *BoltDB) UpdateOrderMetaData(oid order.OrderID, md *db.OrderMetaData) e put(optionsKey, config.Data(md.Options)). put(redemptionReservesKey, uint64Bytes(md.RedemptionReserves)). put(refundReservesKey, uint64Bytes(md.RefundReserves)). + put(accelerationsKey, accelerationsB). err() }) } diff --git a/client/db/types.go b/client/db/types.go index e01475e6a1..63fc370663 100644 --- a/client/db/types.go +++ b/client/db/types.go @@ -285,6 +285,9 @@ type OrderMetaData struct { // to this order, and determining how many more possible refunds there // could be. RefundReserves uint64 + // AccelerationCoins keeps track of all the change coins generated from doing + // accelerations on this order. + AccelerationCoins []order.CoinID } // MetaMatch is a match and its metadata. diff --git a/client/webserver/api.go b/client/webserver/api.go index 30f39a226f..38364cd0a9 100644 --- a/client/webserver/api.go +++ b/client/webserver/api.go @@ -606,6 +606,85 @@ func (s *WebServer) apiOrders(w http.ResponseWriter, r *http.Request) { }, s.indent) } +// apiAccelerateOrder uses the Child-Pays-For-Parent technique to speen up an +// order. +func (s *WebServer) apiAccelerateOrder(w http.ResponseWriter, r *http.Request) { + form := struct { + Pass encode.PassBytes `json:"pw"` + OrderID dex.Bytes `json:"orderID"` + NewRate uint64 `json:"newRate"` + }{} + + defer form.Pass.Clear() + if !readPost(w, r, &form) { + return + } + + txID, err := s.core.AccelerateOrder(form.Pass, form.OrderID, form.NewRate) + if err != nil { + s.writeAPIError(w, fmt.Errorf("Accelerate Order error: %w", err)) + return + } + + writeJSON(w, &struct { + OK bool `json:"ok"` + TxID string `json:"txID"` + }{ + OK: true, + TxID: txID, + }, s.indent) +} + +// apiPreAccelerate responds with information about accelerating the mining of +// swaps in an order +func (s *WebServer) apiPreAccelerate(w http.ResponseWriter, r *http.Request) { + var oid dex.Bytes + if !readPost(w, r, &oid) { + return + } + + preAccelerate, err := s.core.PreAccelerateOrder(oid) + if err != nil { + s.writeAPIError(w, fmt.Errorf("Pre accelerate error: %w", err)) + return + } + + writeJSON(w, &struct { + OK bool `json:"ok"` + PreAccelerate *core.PreAccelerate `json:"preAccelerate"` + }{ + OK: true, + PreAccelerate: preAccelerate, + }, s.indent) +} + +// apiAccelerationEstimate responds with how much it would cost to accelerate +// an order to the requested fee rate. +func (s *WebServer) apiAccelerationEstimate(w http.ResponseWriter, r *http.Request) { + form := struct { + OrderID dex.Bytes `json:"orderID"` + NewRate uint64 `json:"newRate"` + }{} + + if !readPost(w, r, &form) { + return + } + + fee, err := s.core.AccelerationEstimate(form.OrderID, form.NewRate) + if err != nil { + s.writeAPIError(w, fmt.Errorf("Accelerate Order error: %w", err)) + return + } + + writeJSON(w, &struct { + OK bool `json:"ok"` + Fee uint64 `json:"fee"` + }{ + OK: true, + Fee: fee, + }, s.indent) +} + // apiOrder responds with data for an order. func (s *WebServer) apiOrder(w http.ResponseWriter, r *http.Request) { var oid dex.Bytes diff --git a/client/webserver/http.go b/client/webserver/http.go index 9d1b914038..7db5045930 100644 --- a/client/webserver/http.go +++ b/client/webserver/http.go @@ -440,5 +440,4 @@ func (s *WebServer) orderReader(ord *core.Order) *core.OrderReader { BaseUnitInfo: unitInfo(ord.BaseID, ord.BaseSymbol), QuoteUnitInfo: unitInfo(ord.QuoteID, ord.QuoteSymbol), } - } diff --git a/client/webserver/live_test.go b/client/webserver/live_test.go index b55ca7469a..d4cb70df15 100644 --- a/client/webserver/live_test.go +++ b/client/webserver/live_test.go @@ -1354,7 +1354,15 @@ func (c *TCore) Wallets() []*core.WalletState { } return states } - +func (c *TCore) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (string, error) { + return "", nil +} +func (c *TCore) AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, error) { + return 0, nil +} +func (c *TCore) PreAccelerateOrder(oidB dex.Bytes) (*core.PreAccelerate, error) { + return nil, nil +} func (c *TCore) WalletSettings(assetID uint32) (map[string]string, error) { return c.wallets[assetID].settings, nil } diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index f52e16ad99..254acfd9c9 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -215,4 +215,12 @@ var EnUS = map[string]string{ "Maximum Possible Swap Fees": "Maximum Possible Swap Fees", "max_fee_conditions": "This is the most you would ever pay in fees on your swap. Fees are normally assessed at a fraction of this rate. The maximum is not subject to changes once your order is placed.", "wallet_logs": "Wallet Logs", + "accelerate_order": "Accelerate Order", + "acceleration_text": "In case your swap transactions are stuck, you can accelerate them using the Child Pays For Parent technique. When you submit this form, you will create a transaction that sends the change recieved from initiating swaps to yourself with a higher fee. With the new transaction, the average fee rate of all unmined swap transactions, and the new acceleration transaction, will be the rate that you select below.", + "avg_swap_tx_rate": "Average swap tx fee rate", + "current_fee": "Current suggested fee rate", + "accelerate_success": `Successfully submitted transaction: `, + "accelerate": "Accelerate", + "acceleration_transactions": "Acceleration Transactions", + "acceleration_cost_msg": `Increasing the fee rate to will cost `, } diff --git a/client/webserver/site/src/css/order.scss b/client/webserver/site/src/css/order.scss index 4c134263d2..22f6fb0193 100644 --- a/client/webserver/site/src/css/order.scss +++ b/client/webserver/site/src/css/order.scss @@ -43,3 +43,39 @@ div.match-card { opacity: 0.5; font-family: $demi-sans; } + +#accelerateForm { + width: 500px; +} + +.slider-container { + border: 1px solid #424242; + border-radius: 3px; + padding: 4px; + margin-top: 8px; + opacity: 0.7; + + .xy-range-input { + width: 35px; + font-size: 14px; + height: 16px; + } + + .slider { + margin: 10px 10px 5px; + height: 2px; + background-color: grey; + position: relative; + + .slider-handle { + position: absolute; + height: 20px; + width: 14px; + top: -9px; + border-radius: 7px; + background-color: #2cce9c; + border: 2px solid #424242; + cursor: pointer; + } + } +} diff --git a/client/webserver/site/src/html/order.tmpl b/client/webserver/site/src/html/order.tmpl index 8a0a12e951..3eaa54c268 100644 --- a/client/webserver/site/src/html/order.tmpl +++ b/client/webserver/site/src/html/order.tmpl @@ -84,6 +84,10 @@
[[[Age]]]
+
+
[[[accelerate]]]
+
+
{{- /* END DATA CARDS */ -}} {{- /* MATCHES */ -}} @@ -203,13 +207,82 @@ +
+
[[[acceleration_transactions]]]
+
+ {{range $ord.AccelerationCoins}} + {{.StringID}}
+ {{end}} +
+
+
{{template "cancelOrderForm" .}}
+
+ {{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+
+ [[[accelerate_order]]] +
+
+ [[[acceleration_text]]] +
+
+ [[[avg_swap_tx_rate]]]: +
+
+ [[[current_fee]]]: +
+
+
+
+ [[[acceleration_cost_msg]]] +
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ [[[accelerate_success]]] +
+
+
+ +
+
+ +
+
+
+ +
+
+ + + + + + + + +
+
+ {{template "bottom"}} {{end}} diff --git a/client/webserver/site/src/js/order.ts b/client/webserver/site/src/js/order.ts index b99dfd7994..1b94838a29 100644 --- a/client/webserver/site/src/js/order.ts +++ b/client/webserver/site/src/js/order.ts @@ -11,7 +11,8 @@ import { OrderNote, MatchNote, Match, - Coin + Coin, + XYRange } from './registry' const Mainnet = 0 @@ -22,12 +23,20 @@ const animationLength = 500 let net: number +interface PreAccelerate { + swapRate: number + suggestedRate: number + suggestedRange: XYRange +} + export default class OrderPage extends BasePage { orderID: string order: Order page: Record currentForm: HTMLElement secondTicker: number + acceleratedRate: number + refreshOnPopupClose: boolean constructor (main: HTMLElement) { super() @@ -43,15 +52,38 @@ export default class OrderPage extends BasePage { const page = this.page = Doc.idDescendants(main) + page.forms.querySelectorAll('.form-closer').forEach(el => { + Doc.bind(el, 'click', () => { + if (this.refreshOnPopupClose) { + location.replace(location.href) + return + } + Doc.hide(page.forms) + }) + }) + if (page.cancelBttn) { Doc.bind(page.cancelBttn, 'click', () => { this.showForm(page.cancelForm) }) } + Doc.cleanTemplates(page.rangeOptTmpl) + Doc.bind(page.accelerateBttn, 'click', () => { + this.showAccelerateForm() + }) + Doc.bind(page.accelerateSubmit, 'click', () => { + this.submitAccelerate() + }) + this.showAccelerationDiv() + // If the user clicks outside of a form, it should close the page overlay. Doc.bind(page.forms, 'mousedown', (e: MouseEvent) => { if (!Doc.mouseInElement(e, this.currentForm)) { + if (this.refreshOnPopupClose) { + location.reload() + return + } Doc.hide(page.forms) page.cancelPass.value = '' } @@ -107,7 +139,7 @@ export default class OrderPage extends BasePage { async showForm (form: HTMLElement) { this.currentForm = form const page = this.page - Doc.hide(page.cancelForm) + Doc.hide(page.cancelForm, page.accelerateForm) form.style.right = '10000px' Doc.show(page.forms, form) const shift = (page.forms.offsetWidth + form.offsetWidth) / 2 @@ -117,6 +149,120 @@ export default class OrderPage extends BasePage { form.style.right = '0px' } + /* + * showAccelerationDiv shows the acceleration button if the "from" asset's + * wallet supports acceleration and the order has unconfirmed swap transactions + */ + showAccelerationDiv () { + const order = this.order + if (!order) return + const page = this.page + const canAccelerateOrder: () => boolean = () => { + const walletTraitAccelerator = 1 << 4 + let fromAssetID + if (order.sell) fromAssetID = order.baseID + else fromAssetID = order.quoteID + const wallet = app().walletMap[fromAssetID] + if (!wallet || !(wallet.traits & walletTraitAccelerator)) return false + if (order.matches) { + for (let i = 0; i < order.matches.length; i++) { + const match = order.matches[i] + if (match.swap && match.swap.confs && match.swap.confs.count === 0) { + return true + } + } + } + return false + } + if (canAccelerateOrder()) Doc.show(page.accelerateDiv) + else Doc.hide(page.accelerateDiv) + } + + /* showAccelerateForm shows a form to accelerate an order */ + async showAccelerateForm () { + const page = this.page + const order = this.order + while (page.sliderContainer.firstChild) { + page.sliderContainer.removeChild(page.sliderContainer.firstChild) + } + const loaded = app().loading(page.accelerateDiv) + const res = await postJSON('/api/preaccelerate', order.id) + loaded() + if (!app().checkResponse(res)) { + page.preAccelerateErr.textContent = `Error accelerating order: ${res.msg}` + Doc.hide(page.accelerateMainDiv, page.accelerateSuccess) + Doc.show(page.accelerateMsgDiv, page.preAccelerateErr) + this.showForm(page.accelerateForm) + return + } + Doc.hide(page.accelerateMsgDiv, page.preAccelerateErr, page.accelerateErr, page.feeEstimateDiv) + Doc.show(page.accelerateMainDiv, page.accelerateSuccess) + const preAccelerate: PreAccelerate = res.preAccelerate + page.accelerateAvgFeeRate.textContent = `${preAccelerate.swapRate} ${preAccelerate.suggestedRange.yUnit}` + page.accelerateCurrentFeeRate.textContent = `${preAccelerate.suggestedRate} ${preAccelerate.suggestedRange.yUnit}` + OrderUtil.setOptionTemplates(page) + this.acceleratedRate = preAccelerate.suggestedRange.start.y + const updated = (_: number, newY: number) => { this.acceleratedRate = newY } + const changed = async () => { + const req = { + orderID: order.id, + newRate: this.acceleratedRate + } + const loaded = app().loading(page.sliderContainer) + const res = await postJSON('/api/accelerationestimate', req) + loaded() + if (!app().checkResponse(res)) { + page.accelerateErr.textContent = `Error estimating acceleration fee: ${res.msg}` + Doc.show(page.accelerateErr) + return + } + page.feeRateEstimate.textContent = `${this.acceleratedRate} ${preAccelerate.suggestedRange.yUnit}` + let assetID + let assetSymbol + if (order.sell) { + assetID = order.baseID + assetSymbol = order.baseSymbol + } else { + assetID = order.quoteID + assetSymbol = order.quoteSymbol + } + const unitInfo = app().unitInfo(assetID) + page.feeEstimate.textContent = `${res.fee / unitInfo.conventional.conversionFactor} ${assetSymbol}` + Doc.show(page.feeEstimateDiv) + } + const selected = () => { /* do nothing */ } + const roundY = true + const rangeHandler = new OrderUtil.XYRangeHandler(preAccelerate.suggestedRange, + preAccelerate.suggestedRange.start.x, updated, changed, selected, roundY) + page.sliderContainer.appendChild(rangeHandler.control) + changed() + this.showForm(page.accelerateForm) + } + + /* submitAccelerate sends a request to accelerate an order */ + async submitAccelerate () { + const order = this.order + const page = this.page + const req = { + pw: page.acceleratePass.value, + orderID: order.id, + newRate: this.acceleratedRate + } + page.acceleratePass.value = '' + const loaded = app().loading(page.accelerateForm) + const res = await postJSON('/api/accelerateorder', req) + loaded() + if (app().checkResponse(res)) { + this.refreshOnPopupClose = true + page.accelerateTxID.textContent = res.txID + Doc.hide(page.accelerateMainDiv, page.preAccelerateErr, page.accelerateErr) + Doc.show(page.accelerateMsgDiv, page.accelerateSuccess) + } else { + page.accelerateErr.textContent = `Error accelerating order: ${res.msg}` + Doc.show(page.accelerateErr) + } + } + /* submitCancel submits a cancellation for the order. */ async submitCancel () { // this will be the page.cancelSubmit button (evt.currentTarget) @@ -141,13 +287,16 @@ export default class OrderPage extends BasePage { * used to update an order's status. */ handleOrderNote (note: OrderNote) { + const page = this.page const order = note.order - const bttn = this.page.cancelBttn + this.order = order + const bttn = page.cancelBttn if (bttn && order.id === this.orderID) { if (bttn && order.status > OrderUtil.StatusBooked) Doc.hide(bttn) - this.page.status.textContent = OrderUtil.statusString(order) + page.status.textContent = OrderUtil.statusString(order) } for (const m of order.matches || []) this.processMatch(m) + this.showAccelerationDiv() } /* handleMatchNote handles a 'match' notification. */ diff --git a/client/webserver/site/src/js/orderutil.ts b/client/webserver/site/src/js/orderutil.ts index a6e1ee02a4..6510218bfb 100644 --- a/client/webserver/site/src/js/orderutil.ts +++ b/client/webserver/site/src/js/orderutil.ts @@ -6,7 +6,8 @@ import { TradeForm, PageElement, OrderOption as OrderOpt, - Match + Match, + XYRange } from './registry' export const Limit = 1 @@ -222,12 +223,12 @@ class BooleanOrderOption extends OrderOption { } /* - * XYRangeOrderOption is the handler for an *XYRange from client/asset. XYRange - * has a slider which allows adjusting the x and y, linearly between two limits. - * The user can also manually enter values for x or y. + * XYRangeOrderOption is an order option that contains an XYRangeHandler. + * The logic for handling the slider to is defined in XYRangeHandler so + * that the slider can be used without being contained in an order option. */ class XYRangeOrderOption extends OrderOption { - control: HTMLElement + handler: XYRangeHandler x: number changed: () => void @@ -237,12 +238,54 @@ class XYRangeOrderOption extends OrderOption { disable: () => this.disable() }) this.changed = changed + const cfg = opt.xyRange + const setVal = order.options[opt.key] + this.on = typeof setVal !== 'undefined' + if (this.on) { + this.node.classList.add('selected') + this.x = setVal + } else { + this.x = opt.default + } + const onUpdate = (x: number) => { this.order.options[this.opt.key] = x } + const onChange = () => { this.changed() } + const selected = () => { this.node.classList.add('selected') } + this.handler = new XYRangeHandler(cfg, this.x, onUpdate, onChange, selected) + this.tmpl.controls.appendChild(this.handler.control) + } + + enable () { + this.order.options[this.opt.key] = this.x + this.changed() + } + + disable () { + delete this.order.options[this.opt.key] + this.changed() + } +} + +/* + * XYRangeHandler is the handler for an *XYRange from client/asset. XYRange + * has a slider which allows adjusting the x and y, linearly between two limits. + * The user can also manually enter values for x or y. + */ +export class XYRangeHandler { + control: HTMLElement + x: number + updated: (x:number, y:number) => void + changed: () => void + selected: () => void + + constructor (cfg: XYRange, initVal: number, updated: (x:number, y:number) => void, changed: () => void, selected: () => void, roundY?: boolean) { const control = this.control = rangeOptTmpl.cloneNode(true) as HTMLElement const tmpl = Doc.parseTemplate(control) - const cfg = opt.xyRange + + this.changed = changed + this.selected = selected + this.updated = updated + const { slider, handle } = tmpl - // Append to parent's options div. - this.tmpl.controls.appendChild(control) const rangeX = cfg.end.x - cfg.start.x const rangeY = cfg.end.y - cfg.start.y @@ -250,8 +293,8 @@ class XYRangeOrderOption extends OrderOption { // r, x, and y will be updated by the various input event handlers. r is // x (or y) normalized on its range, e.g. [x_min, x_max] -> [0, 1] - let r = normalizeX(opt.default) - let x = this.x = opt.default + let r = normalizeX(initVal) + let x = this.x = initVal let y = r * rangeY + cfg.start.y const number = new Intl.NumberFormat((navigator.languages as string[]), { @@ -260,12 +303,14 @@ class XYRangeOrderOption extends OrderOption { }) // accept needs to be called anytime a handler updates x, y, and r. - const accept = (skipStore?: boolean) => { + const accept = (skipUpdate?: boolean) => { + if (roundY) y = Math.round(y) tmpl.x.textContent = number.format(x) tmpl.y.textContent = number.format(y) + if (roundY) tmpl.y.textContent = `${y}` handle.style.left = `calc(${r * 100}% - ${r * 14}px)` this.x = x - if (!skipStore) this.order.options[this.opt.key] = x + if (!skipUpdate) this.updated(x, y) } // Set up the handlers for the x and y text input fields. @@ -331,7 +376,7 @@ class XYRangeOrderOption extends OrderOption { Doc.bind(handle, 'mousedown', (e: MouseEvent) => { if (e.button !== 0) return e.preventDefault() - this.node.classList.add('selected') + this.selected() const startX = e.pageX const w = slider.clientWidth - handle.offsetWidth const startLeft = normalizeX(x) * w @@ -357,29 +402,8 @@ class XYRangeOrderOption extends OrderOption { tmpl.rangeLblEnd.textContent = cfg.end.label tmpl.xUnit.textContent = cfg.xUnit tmpl.yUnit.textContent = cfg.yUnit - - // Set the initial state if this is a selected option. - const setVal = order.options[opt.key] - this.on = typeof setVal !== 'undefined' - if (this.on) { - this.node.classList.add('selected') - x = setVal - r = normalizeX(x) - y = r * rangeY + cfg.start.y - } - accept(true) } - - enable () { - this.order.options[this.opt.key] = this.x - this.changed() - } - - disable () { - delete this.order.options[this.opt.key] - this.changed() - } } /* diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts index 8fd005d05c..00128c8a26 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -73,6 +73,7 @@ export interface Order { canceled: boolean feesPaid: FeeBreakdown fundingCoins: Coin[] + accelerationCoins: Coin[] lockedamt: number rate: number // limit only tif: number // limit only diff --git a/client/webserver/site/src/localized_html/en-US/order.tmpl b/client/webserver/site/src/localized_html/en-US/order.tmpl index 8338ba25e9..4667df4f01 100644 --- a/client/webserver/site/src/localized_html/en-US/order.tmpl +++ b/client/webserver/site/src/localized_html/en-US/order.tmpl @@ -84,6 +84,10 @@
Age
+
+
Accelerate
+
+
{{- /* END DATA CARDS */ -}} {{- /* MATCHES */ -}} @@ -203,13 +207,82 @@ +
+
Acceleration Transactions
+
+ {{range $ord.AccelerationCoins}} + {{.StringID}}
+ {{end}} +
+
+
{{template "cancelOrderForm" .}}
+
+ {{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+
+ Accelerate Order +
+
+ In case your swap transactions are stuck, you can accelerate them using the Child Pays For Parent technique. When you submit this form, you will create a transaction that sends the change recieved from initiating swaps to yourself with a higher fee. With the new transaction, the average fee rate of all unmined swap transactions, and the new acceleration transaction, will be the rate that you select below. +
+
+ Average swap tx fee rate: +
+
+ Current suggested fee rate: +
+
+
+
+ Increasing the fee rate to will cost +
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ Successfully submitted transaction: +
+
+
+ +
+
+ +
+
+
+ +
+
+ + + + + + + + +
+
+ {{template "bottom"}} {{end}} diff --git a/client/webserver/site/src/localized_html/pl-PL/order.tmpl b/client/webserver/site/src/localized_html/pl-PL/order.tmpl index e16795f50c..58110043e2 100644 --- a/client/webserver/site/src/localized_html/pl-PL/order.tmpl +++ b/client/webserver/site/src/localized_html/pl-PL/order.tmpl @@ -84,6 +84,10 @@
Wiek
+
+
Accelerate
+
+
{{- /* END DATA CARDS */ -}} {{- /* MATCHES */ -}} @@ -203,13 +207,82 @@ +
+
Acceleration Transactions
+
+ {{range $ord.AccelerationCoins}} + {{.StringID}}
+ {{end}} +
+
+
{{template "cancelOrderForm" .}}
+
+ {{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+
+ Accelerate Order +
+
+ In case your swap transactions are stuck, you can accelerate them using the Child Pays For Parent technique. When you submit this form, you will create a transaction that sends the change recieved from initiating swaps to yourself with a higher fee. With the new transaction, the average fee rate of all unmined swap transactions, and the new acceleration transaction, will be the rate that you select below. +
+
+ Average swap tx fee rate: +
+
+ Current suggested fee rate: +
+
+
+
+ Increasing the fee rate to will cost +
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ Successfully submitted transaction: +
+
+
+ +
+
+ +
+
+
+ +
+
+ + + + + + + + +
+
+ {{template "bottom"}} {{end}} diff --git a/client/webserver/site/src/localized_html/pt-BR/order.tmpl b/client/webserver/site/src/localized_html/pt-BR/order.tmpl index 5c189713d7..55a1e3fe8a 100644 --- a/client/webserver/site/src/localized_html/pt-BR/order.tmpl +++ b/client/webserver/site/src/localized_html/pt-BR/order.tmpl @@ -84,6 +84,10 @@
Idade
+
+
Accelerate
+
+
{{- /* END DATA CARDS */ -}} {{- /* MATCHES */ -}} @@ -203,13 +207,82 @@ +
+
Acceleration Transactions
+
+ {{range $ord.AccelerationCoins}} + {{.StringID}}
+ {{end}} +
+
+
{{template "cancelOrderForm" .}}
+
+ {{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+
+ Accelerate Order +
+
+ In case your swap transactions are stuck, you can accelerate them using the Child Pays For Parent technique. When you submit this form, you will create a transaction that sends the change recieved from initiating swaps to yourself with a higher fee. With the new transaction, the average fee rate of all unmined swap transactions, and the new acceleration transaction, will be the rate that you select below. +
+
+ Average swap tx fee rate: +
+
+ Current suggested fee rate: +
+
+
+
+ Increasing the fee rate to will cost +
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ Successfully submitted transaction: +
+
+
+ +
+
+ +
+
+
+ +
+
+ + + + + + + + +
+
+ {{template "bottom"}} {{end}} diff --git a/client/webserver/site/src/localized_html/zh-CN/order.tmpl b/client/webserver/site/src/localized_html/zh-CN/order.tmpl index b7aa312bea..0953bc876d 100644 --- a/client/webserver/site/src/localized_html/zh-CN/order.tmpl +++ b/client/webserver/site/src/localized_html/zh-CN/order.tmpl @@ -84,6 +84,10 @@
年龄
+
+
Accelerate
+
+
{{- /* END DATA CARDS */ -}} {{- /* MATCHES */ -}} @@ -203,13 +207,82 @@ +
+
Acceleration Transactions
+
+ {{range $ord.AccelerationCoins}} + {{.StringID}}
+ {{end}} +
+
+
{{template "cancelOrderForm" .}}
+
+ {{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+
+ Accelerate Order +
+
+ In case your swap transactions are stuck, you can accelerate them using the Child Pays For Parent technique. When you submit this form, you will create a transaction that sends the change recieved from initiating swaps to yourself with a higher fee. With the new transaction, the average fee rate of all unmined swap transactions, and the new acceleration transaction, will be the rate that you select below. +
+
+ Average swap tx fee rate: +
+
+ Current suggested fee rate: +
+
+
+
+ Increasing the fee rate to will cost +
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+ Successfully submitted transaction: +
+
+
+ +
+
+ +
+
+
+ +
+
+ + + + + + + + +
+
+ {{template "bottom"}} {{end}} diff --git a/client/webserver/webserver.go b/client/webserver/webserver.go index 5e680c9745..168e24eb87 100644 --- a/client/webserver/webserver.go +++ b/client/webserver/webserver.go @@ -112,6 +112,9 @@ type clientCore interface { PreOrder(*core.TradeForm) (*core.OrderEstimate, error) WalletLogFilePath(assetID uint32) (string, error) EstimateRegistrationTxFee(host string, certI interface{}, assetID uint32) (uint64, error) + PreAccelerateOrder(oidB dex.Bytes) (*core.PreAccelerate, error) + AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (string, error) + AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, error) } var _ clientCore = (*core.Core)(nil) @@ -342,6 +345,9 @@ func New(cfg *Config) (*WebServer, error) { apiAuth.Post("/exportseed", s.apiExportSeed) apiAuth.Post("/importaccount", s.apiAccountImport) apiAuth.Post("/disableaccount", s.apiAccountDisable) + apiAuth.Post("/accelerateorder", s.apiAccelerateOrder) + apiAuth.Post("/preaccelerate", s.apiPreAccelerate) + apiAuth.Post("/accelerationestimate", s.apiAccelerationEstimate) }) }) diff --git a/client/webserver/webserver_test.go b/client/webserver/webserver_test.go index 7f2d523e8e..82c127971e 100644 --- a/client/webserver/webserver_test.go +++ b/client/webserver/webserver_test.go @@ -176,6 +176,15 @@ func (c *TCore) ExportSeed(pw []byte) ([]byte, error) { func (c *TCore) WalletLogFilePath(uint32) (string, error) { return "", nil } +func (c *TCore) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (string, error) { + return "", nil +} +func (c *TCore) AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, error) { + return 0, nil +} +func (c *TCore) PreAccelerateOrder(oidB dex.Bytes) (*core.PreAccelerate, error) { + return nil, nil +} type TWriter struct { b []byte diff --git a/dex/networks/btc/script.go b/dex/networks/btc/script.go index 73325fcb66..3807831eab 100644 --- a/dex/networks/btc/script.go +++ b/dex/networks/btc/script.go @@ -170,6 +170,15 @@ const ( // NOTE: witness data is not script. RedeemP2WPKHInputWitnessWeight = 1 + 1 + DERSigLength + 1 + 33 // 109 + // RedeemP2PKHInputTotalSize is the worst case size of a transaction + // input redeeming a P2WPKH output and the corresponding witness data. + // It is calculated as: + // + // 41 vbytes base tx input + // 109wu witness + 2wu segwit marker and flag = 28 vbytes + // total = 69 vbytes + RedeemP2PWKHInputTotalSize = RedeemP2WPKHInputSize + ((RedeemP2WPKHInputWitnessWeight + 2 + 3) / 4) + // RedeemP2WSHInputWitnessWeight depends on the number of redeem scrpit and // number of signatures. // version + signatures + length of redeem script + redeemscript @@ -218,7 +227,7 @@ const ( // 41 vbytes base tx input // 109wu witness + 2wu segwit marker and flag = 28 vbytes // total = 153 vbytes - InitTxSizeSegwit = InitTxSizeBaseSegwit + RedeemP2WPKHInputSize + ((RedeemP2WPKHInputWitnessWeight + 2 + 3) / 4) + InitTxSizeSegwit = InitTxSizeBaseSegwit + RedeemP2PWKHInputTotalSize witnessWeight = blockchain.WitnessScaleFactor ) From fbe5a2d55fdceeb3f3a6d3db817ad954c4c02ba2 Mon Sep 17 00:00:00 2001 From: martonp Date: Thu, 7 Apr 2022 14:06:54 +0700 Subject: [PATCH 02/13] Changes based on buck's review --- client/asset/btc/btc.go | 16 +++++++++------- client/asset/interface.go | 20 +++++++++++--------- client/core/core.go | 11 ++++++----- client/core/trade.go | 8 ++++---- client/core/wallet.go | 24 ++++++++++++------------ client/webserver/api.go | 8 ++++++-- 6 files changed, 48 insertions(+), 39 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 53505bcfc9..4132dfc95a 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -1949,9 +1949,10 @@ func (btc *baseWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, if newChange != nil { newChangeCoin = newChange - // Checking required for remaining swaps > 0 because this ensures if the previous change - // was locked, this one will also be locked. If requiredForRemainingSwaps = 0, but the - // change was locked, signedAccelerationTx would have returned an error since this means + // Checking required for remaining swaps > 0 because this ensures if + // the previous change was locked, this one will also be locked. If + // requiredForRemainingSwaps = 0, but the change was locked, + // signedAccelerationTx would have returned an error since this means // that the change was locked by another order. if requiredForRemainingSwaps > 0 { err = btc.node.lockUnspent(false, []*output{newChange}) @@ -1989,9 +1990,9 @@ func (btc *baseWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.B return fee, nil } -// PreAccelerate returns the current average fee rate of the unmined swap initiation -// and acceleration transactions, and also returns a suggested range that the -// fee rate should be increased to in order to expedite mining. +// PreAccelerate returns the current average fee rate of the unmined swap +// initiation and acceleration transactions, and also returns a suggested +// range that the fee rate should be increased to in order to expedite mining. func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange asset.XYRange, err error) { makeError := func(err error) (uint64, asset.XYRange, error) { return 0, asset.XYRange{}, err @@ -2220,7 +2221,8 @@ func (btc *baseWallet) lookupOutput(txHash *chainhash.Hash, vout uint32) (*outpu return newOutput(txHash, vout, uint64(value)), nil } -// changeCanBeAccelerated will return an error if the change cannot be accelerated. +// changeCanBeAccelerated returns whether or not the change output can be +// accelerated. func (btc *baseWallet) changeCanBeAccelerated(changeTxHash *chainhash.Hash, changeVout uint32, requiredForRemainingSwaps uint64) (bool, error) { lockedUtxos, err := btc.node.listLockUnspent() if err != nil { diff --git a/client/asset/interface.go b/client/asset/interface.go index ab083c1218..67a57f4dfc 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -374,21 +374,23 @@ type FeeRater interface { // mining of swap transactions. type Accelerator interface { // AccelerateOrder uses the Child-Pays-For-Parent technique to accelerate a - // chain of swap transactions and previous accelerations. It broadcasts a new - // transaction with a fee high enough so that the average fee of all the - // unconfirmed transactions in the chain and the new transaction will have - // an average fee rate of newFeeRate. requiredForRemainingSwaps is passed - // in to ensure that the new change coin will have enough funds to initiate - // the additional swaps that will be required to complete the order. + // chain of swap transactions and previous accelerations. It broadcasts a + // new transaction with a fee high enough so that the average fee of all + // the unconfirmed transactions in the chain and the new transaction will + // have an average fee rate of newFeeRate. requiredForRemainingSwaps is + // passed in to ensure that the new change coin will have enough funds to + // initiate the additional swaps that will be required to complete the + // order. AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (Coin, string, error) // AccelerationEstimate takes the same parameters as AccelerateOrder, but // instead of broadcasting the acceleration transaction, it just returns // the amount of funds that will need to be spent in order to increase the // average fee rate to the desired amount. AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) - // PreAccelerate returns the current average fee rate of the unmined swap initiation - // and acceleration transactions, and also returns a suggested range that the - // fee rate should be increased to in order to expedite mining. + // PreAccelerate returns the current average fee rate of the unmined swap + // initiation and acceleration transactions, and also returns a suggested + // range that the fee rate should be increased to in order to expedite + // mining. PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange XYRange, err error) } diff --git a/client/core/core.go b/client/core/core.go index 680a92cc3c..d7ba4d86c9 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -2035,7 +2035,7 @@ func (c *Core) RescanWallet(assetID uint32, force bool) error { } // Begin potentially asynchronous wallet rescan operation. - if err = wallet.Rescan(c.ctx); err != nil { + if err = wallet.rescan(c.ctx); err != nil { return err } @@ -6863,7 +6863,7 @@ func (c *Core) WalletLogFilePath(assetID uint32) (string, error) { strings.ToUpper(unbip(assetID)), assetID) } - return wallet.LogFilePath() + return wallet.logFilePath() } func createFile(fileName string) (*os.File, error) { @@ -7144,7 +7144,7 @@ func (c *Core) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (st } newChangeCoin, txID, err := - tracker.wallets.fromWallet.AccelerateOrder(swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, newFeeRate) + tracker.wallets.fromWallet.accelerateOrder(swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, newFeeRate) if err != nil { return "", err } @@ -7168,7 +7168,7 @@ func (c *Core) AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, return 0, err } - accelerationFee, err := tracker.wallets.fromWallet.AccelerationEstimate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) + accelerationFee, err := tracker.wallets.fromWallet.accelerationEstimate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) if err != nil { return 0, err } @@ -7187,7 +7187,7 @@ func (c *Core) PreAccelerateOrder(oidB dex.Bytes) (*PreAccelerate, error) { feeSuggestion := c.feeSuggestionAny(tracker.fromAssetID) currentRate, suggestedRange, err := - tracker.wallets.fromWallet.PreAccelerate(swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, feeSuggestion) + tracker.wallets.fromWallet.preAccelerate(swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, feeSuggestion) if err != nil { return nil, err } @@ -7228,6 +7228,7 @@ func (c *Core) orderAccelerationParameters(oidB dex.Bytes) (tracker *trackedTrad swapSize := fromAsset.SwapSize lotsRemaining := tracker.Trade().Remaining() / lotSize requiredForRemainingSwaps = lotsRemaining * swapSize * tracker.metaData.MaxFeeRate + break } } if tracker == nil { diff --git a/client/core/trade.go b/client/core/trade.go index 55b48a864d..41f23896df 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -866,7 +866,7 @@ func (t *trackedTrade) counterPartyConfirms(ctx context.Context, match *matchTra } expired = time.Until(lockTime) < 0 // not necessarily refundable, but can be at any moment - have, spent, err = wallet.SwapConfirmations(ctx, coin.ID(), + have, spent, err = wallet.swapConfirmations(ctx, coin.ID(), match.MetaData.Proof.CounterContract, match.MetaData.Stamp) if err != nil { return fail(fmt.Errorf("failed to get confirmations of the counter-party's swap %s (%s) "+ @@ -1111,7 +1111,7 @@ func (t *trackedTrade) isSwappable(ctx context.Context, match *matchTracker) boo // If we're the maker, check the confirmations anyway so we can notify. t.dc.log.Tracef("Checking confirmations on our OWN swap txn %v (%s)...", coinIDString(wallet.AssetID, match.MetaData.Proof.MakerSwap), unbip(wallet.AssetID)) - confs, spent, err := wallet.SwapConfirmations(ctx, match.MetaData.Proof.MakerSwap, + confs, spent, err := wallet.swapConfirmations(ctx, match.MetaData.Proof.MakerSwap, match.MetaData.Proof.ContractData, match.MetaData.Stamp) if err != nil && !errors.Is(err, asset.ErrSwapNotInitiated) { // No need to log an error if swap not initiated as this @@ -1199,7 +1199,7 @@ func (t *trackedTrade) isRedeemable(ctx context.Context, match *matchTracker) bo } // If we're the taker, check the confirmations anyway so we can notify. - confs, spent, err := t.wallets.fromWallet.SwapConfirmations(ctx, match.MetaData.Proof.TakerSwap, + confs, spent, err := t.wallets.fromWallet.swapConfirmations(ctx, match.MetaData.Proof.TakerSwap, match.MetaData.Proof.ContractData, match.MetaData.Stamp) if err != nil && !errors.Is(err, asset.ErrSwapNotInitiated) { // No need to log an error if swap not initiated as this @@ -1328,7 +1328,7 @@ func (t *trackedTrade) shouldBeginFindRedemption(ctx context.Context, match *mat return false } - confs, spent, err := t.wallets.fromWallet.SwapConfirmations(ctx, swapCoinID, proof.ContractData, match.MetaData.Stamp) + confs, spent, err := t.wallets.fromWallet.swapConfirmations(ctx, swapCoinID, proof.ContractData, match.MetaData.Stamp) if err != nil { if !errors.Is(err, asset.ErrSwapNotInitiated) { // No need to log an error if swap not initiated as this diff --git a/client/core/wallet.go b/client/core/wallet.go index ff5d215b39..16372bf81d 100644 --- a/client/core/wallet.go +++ b/client/core/wallet.go @@ -282,9 +282,9 @@ func (w *xcWallet) Disconnect() { w.mtx.Unlock() } -// Rescan will initiate a rescan of the wallet if the asset.Wallet +// rescan will initiate a rescan of the wallet if the asset.Wallet // implementation is a Rescanner. -func (w *xcWallet) Rescan(ctx context.Context) error { +func (w *xcWallet) rescan(ctx context.Context) error { rescanner, ok := w.Wallet.(asset.Rescanner) if !ok { return errors.New("wallet does not support rescanning") @@ -292,9 +292,9 @@ func (w *xcWallet) Rescan(ctx context.Context) error { return rescanner.Rescan(ctx) } -// LogFilePath returns the path of the wallet's log file if the +// logFilePath returns the path of the wallet's log file if the // asset.Wallet implementation is a LogFiler. -func (w *xcWallet) LogFilePath() (string, error) { +func (w *xcWallet) logFilePath() (string, error) { logFiler, ok := w.Wallet.(asset.LogFiler) if !ok { return "", errors.New("wallet does not support getting log file") @@ -302,9 +302,9 @@ func (w *xcWallet) LogFilePath() (string, error) { return logFiler.LogFilePath(), nil } -// AccelerateOrder uses the Child-Pays-For-Parent technique to accelerate an +// accelerateOrder uses the Child-Pays-For-Parent technique to accelerate an // order if the wallet is an Accelerator. -func (w *xcWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { +func (w *xcWallet) accelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { accelerator, ok := w.Wallet.(asset.Accelerator) if !ok { return nil, "", errors.New("wallet does not support acceleration") @@ -312,9 +312,9 @@ func (w *xcWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, cha return accelerator.AccelerateOrder(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) } -// AccelerationEstimate estimates the cost to accelerate an order if the wallet +// accelerationEstimate estimates the cost to accelerate an order if the wallet // is an Accelerator. -func (w *xcWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, error) { +func (w *xcWallet) accelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, error) { accelerator, ok := w.Wallet.(asset.Accelerator) if !ok { return 0, errors.New("wallet does not support acceleration") @@ -323,9 +323,9 @@ func (w *xcWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes return accelerator.AccelerationEstimate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion) } -// PreAccelerate gives the user information about accelerating an order if the +// preAccelerate gives the user information about accelerating an order if the // wallet is an Accelerator. -func (w *xcWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange asset.XYRange, err error) { +func (w *xcWallet) preAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange asset.XYRange, err error) { accelerator, ok := w.Wallet.(asset.Accelerator) if !ok { return 0, asset.XYRange{}, errors.New("wallet does not support acceleration") @@ -334,11 +334,11 @@ func (w *xcWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, chang return accelerator.PreAccelerate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion) } -// SwapConfirmations calls (asset.Wallet).SwapConfirmations with a timeout +// swapConfirmations calls (asset.Wallet).SwapConfirmations with a timeout // Context. If the coin cannot be located, an asset.CoinNotFoundError is // returned. If the coin is located, but recognized as spent, no error is // returned. -func (w *xcWallet) SwapConfirmations(ctx context.Context, coinID []byte, contract []byte, matchTime uint64) (uint32, bool, error) { +func (w *xcWallet) swapConfirmations(ctx context.Context, coinID []byte, contract []byte, matchTime uint64) (uint32, bool, error) { return w.Wallet.SwapConfirmations(ctx, coinID, contract, encode.UnixTimeMilli(int64(matchTime))) } diff --git a/client/webserver/api.go b/client/webserver/api.go index 38364cd0a9..63f3f21d20 100644 --- a/client/webserver/api.go +++ b/client/webserver/api.go @@ -614,13 +614,17 @@ func (s *WebServer) apiAccelerateOrder(w http.ResponseWriter, r *http.Request) { OrderID dex.Bytes `json:"orderID"` NewRate uint64 `json:"newRate"` }{} - defer form.Pass.Clear() if !readPost(w, r, &form) { return } + pass, err := s.resolvePass(form.Pass, r) + if err != nil { + s.writeAPIError(w, fmt.Errorf("password error: %w", err)) + return + } - txID, err := s.core.AccelerateOrder(form.Pass, form.OrderID, form.NewRate) + txID, err := s.core.AccelerateOrder(pass, form.OrderID, form.NewRate) if err != nil { s.writeAPIError(w, fmt.Errorf("Accelerate Order error: %w", err)) return From 6a0b2ef2e5cb43646732b12f5b0474468fa40a45 Mon Sep 17 00:00:00 2001 From: martonp Date: Sat, 16 Apr 2022 17:33:13 +0700 Subject: [PATCH 03/13] Updates based on Joe's review. --- client/asset/btc/btc.go | 115 ++++++++++++++++++++++++----- client/asset/btc/btc_test.go | 139 ++++++++++++++++++++++++++++++++++- client/asset/btc/spv_test.go | 3 +- client/asset/interface.go | 4 +- client/core/core.go | 119 +++++++++++++++++++----------- dex/order/order.go | 10 +++ 6 files changed, 326 insertions(+), 64 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 4132dfc95a..a1a683102f 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -72,6 +72,10 @@ const ( redeemFeeBumpFee = "redeemfeebump" ) +const ( + minTimeBeforeAcceleration uint64 = 3600 // 1 hour +) + var ( // ContractSearchLimit is how far back in time AuditContract in SPV mode // will search for a contract if no txData is provided. This should be a @@ -586,6 +590,7 @@ type baseWallet struct { estimateFee func(RawRequester, uint64) (uint64, error) // TODO: resolve the awkwardness of an RPC-oriented func in a generic framework decodeAddr dexbtc.AddressDecoder stringAddr dexbtc.AddressStringer + net dex.Network tipMtx sync.RWMutex currentTip *block @@ -856,6 +861,7 @@ func newUnconnectedWallet(cfg *BTCCloneCFG, walletCfg *WalletConfig) (*baseWalle decodeAddr: addrDecoder, stringAddr: addrStringer, walletInfo: cfg.WalletInfo, + net: cfg.Network, } if w.estimateFee == nil { @@ -1931,6 +1937,8 @@ func (btc *baseWallet) fundedTx(coins asset.Coins) (*wire.MsgTx, uint64, []outPo // an average fee rate of newFeeRate. requiredForRemainingSwaps is passed // in to ensure that the new change coin will have enough funds to initiate // the additional swaps that will be required to complete the order. +// +// The returned change coin may be nil, and should be checked before use. func (btc *baseWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { btc.fundingMtx.Lock() defer btc.fundingMtx.Unlock() @@ -1990,6 +1998,43 @@ func (btc *baseWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.B return fee, nil } +// tooEarlyChecks returns true if minTimeBeforeAcceleration has not passed +// since either the earliest unconfirmed transaction in the chain, or the +// latest acceleration transaction. It also returns the minimum time when +// the user can do an acceleration. +func tooEarlyToAccelerate(sortedTxChain []*GetTransactionResult, accelerationCoins []dex.Bytes) (bool, uint64, error) { + accelerationTxs := make(map[string]bool, len(accelerationCoins)) + for _, accelerationCoin := range accelerationCoins { + txHash, _, err := decodeCoinID(accelerationCoin) + if err != nil { + return false, 0, err + } + accelerationTxs[txHash.String()] = true + } + + var timeToCompare uint64 + for _, tx := range sortedTxChain { + if tx.Confirmations > 0 { + continue + } + if timeToCompare == 0 { + timeToCompare = tx.Time + continue + } + if accelerationTxs[tx.TxID] { + timeToCompare = tx.Time + } + } + + if timeToCompare == 0 { + return false, 0, fmt.Errorf("no need to accelerate because all tx are confirmed") + } + + currentTime := uint64(time.Now().Unix()) + minAccelerationTime := timeToCompare + minTimeBeforeAcceleration + return minAccelerationTime > currentTime, minAccelerationTime, nil +} + // PreAccelerate returns the current average fee rate of the unmined swap // initiation and acceleration transactions, and also returns a suggested // range that the fee rate should be increased to in order to expedite mining. @@ -2003,13 +2048,10 @@ func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, c return makeError(err) } - can, err := btc.changeCanBeAccelerated(changeTxHash, changeVout, requiredForRemainingSwaps) + err = btc.changeCanBeAccelerated(changeTxHash, changeVout, requiredForRemainingSwaps) if err != nil { return makeError(err) } - if !can { - return makeError(errors.New("change cannot be accelerated")) - } sortedTxChain, err := btc.sortedTxChain(swapCoins, accelerationCoins, changeCoin) if err != nil { @@ -2029,6 +2071,22 @@ func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, c swapTxsSize += dexbtc.MsgTxVBytes(msgTx) } + // Is it safe to assume that transactions will all have some fee? + if feesAlreadyPaid == 0 { + return makeError(fmt.Errorf("all transactions are already confirmed, no need to accelerate")) + } + + if btc.net != dex.Simnet { + tooEarly, minAccelerationTime, err := tooEarlyToAccelerate(sortedTxChain, accelerationCoins) + if err != nil { + return makeError(err) + } + if tooEarly { + minLocalTime := time.Unix(int64(minAccelerationTime), 0).Local() + return makeError(fmt.Errorf("cannot accelerate until %v", minLocalTime)) + } + } + btc.fundingMtx.RLock() utxos, _, utxosVal, err := btc.spendableUTXOs(1) btc.fundingMtx.RUnlock() @@ -2036,6 +2094,15 @@ func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, c return makeError(err) } + // This is to avoid having too many inputs, and causing an error during signing + if len(utxos) > 500 { + utxos = utxos[len(utxos)-500:] + utxosVal = 0 + for _, utxo := range utxos { + utxosVal += utxo.amount + } + } + var coins asset.Coins for _, utxo := range utxos { coins = append(coins, newOutput(utxo.txHash, utxo.vout, utxo.amount)) @@ -2111,13 +2178,10 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B return makeError(err) } - can, err := btc.changeCanBeAccelerated(changeTxHash, changeVout, requiredForRemainingSwaps) + err = btc.changeCanBeAccelerated(changeTxHash, changeVout, requiredForRemainingSwaps) if err != nil { return makeError(err) } - if !can { - return makeError(errors.New("change cannot be accelerated")) - } sortedTxChain, err := btc.sortedTxChain(swapCoins, accelerationCoins, changeCoin) if err != nil { @@ -2126,7 +2190,7 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B changeOutput, err := btc.lookupOutput(changeTxHash, changeVout) if err != nil { - return makeError(nil) + return makeError(err) } additionalFeesRequired, err := btc.additionalFeesRequired(sortedTxChain, newFeeRate) @@ -2134,6 +2198,21 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B return makeError(err) } + if additionalFeesRequired <= 0 { + return makeError(fmt.Errorf("no additional fees are required to move the fee rate to %v", newFeeRate)) + } + + if btc.net != dex.Simnet { + tooEarly, minAccelerationTime, err := tooEarlyToAccelerate(sortedTxChain, accelerationCoins) + if err != nil { + return makeError(err) + } + if tooEarly { + minLocalTime := time.Unix(int64(minAccelerationTime), 0).Local() + return makeError(fmt.Errorf("cannot accelerate until %v", minLocalTime)) + } + } + txSize := dexbtc.MinimumTxOverhead if btc.segwit { txSize += dexbtc.RedeemP2PWKHInputTotalSize @@ -2213,8 +2292,8 @@ func (btc *baseWallet) lookupOutput(txHash *chainhash.Hash, vout uint32) (*outpu return nil, err } if len(tx.TxOut) <= int(vout) { - return nil, fmt.Errorf("txId %x only has %d outputs. tried to access index %d", - &txHash, len(tx.TxOut), vout) + return nil, fmt.Errorf("txId %s only has %d outputs. tried to access index %d", + txHash, len(tx.TxOut), vout) } value := tx.TxOut[vout].Value @@ -2223,10 +2302,10 @@ func (btc *baseWallet) lookupOutput(txHash *chainhash.Hash, vout uint32) (*outpu // changeCanBeAccelerated returns whether or not the change output can be // accelerated. -func (btc *baseWallet) changeCanBeAccelerated(changeTxHash *chainhash.Hash, changeVout uint32, requiredForRemainingSwaps uint64) (bool, error) { +func (btc *baseWallet) changeCanBeAccelerated(changeTxHash *chainhash.Hash, changeVout uint32, requiredForRemainingSwaps uint64) error { lockedUtxos, err := btc.node.listLockUnspent() if err != nil { - return false, err + return err } var changeIsLocked bool @@ -2238,14 +2317,13 @@ func (btc *baseWallet) changeCanBeAccelerated(changeTxHash *chainhash.Hash, chan } if changeIsLocked && requiredForRemainingSwaps == 0 { - btc.log.Error("change cannot be accelerated because it is locked by another order") - return false, nil + return errors.New("change cannot be accelerated because it is locked by another order") } if !changeIsLocked { utxos, err := btc.node.listUnspent() if err != nil { - return false, err + return err } var changeIsUnspent bool @@ -2257,12 +2335,11 @@ func (btc *baseWallet) changeCanBeAccelerated(changeTxHash *chainhash.Hash, chan } if !changeIsUnspent { - btc.log.Error("change cannot be accelerated because it has already been spent") - return false, nil + return errors.New("change cannot be accelerated because it has already been spent") } } - return true, nil + return nil } // sortedTxChain takes a list of swap coins, acceleration tx IDs, and a change diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index 7aba01f406..27cf5d1de6 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -3163,7 +3163,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { return totalSize } - loadTxsIntoNode := func(txs []*wire.MsgTx, fees []float64, confs []uint64, node *testData, t *testing.T) { + loadTxsIntoNode := func(txs []*wire.MsgTx, fees []float64, confs []uint64, node *testData, withinTimeLimit bool, t *testing.T) { t.Helper() if len(txs) != len(fees) || len(txs) != len(confs) { t.Fatalf("len(txs) = %d, len(fees) = %d, len(confs) = %d", len(txs), len(fees), len(confs)) @@ -3178,6 +3178,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { serializedTxs = append(serializedTxs, serializedTx) } + currentTime := time.Now().Unix() for i := range txs { var blockHash string if confs[i] == 1 { @@ -3189,6 +3190,12 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { BlockHash: blockHash, Fee: fees[i], Confirmations: confs[i]} + + if withinTimeLimit { + node.getTransactionMap[txs[i].TxHash().String()].Time = uint64(currentTime) - minTimeBeforeAcceleration + 3 + } else { + node.getTransactionMap[txs[i].TxHash().String()].Time = uint64(currentTime) - minTimeBeforeAcceleration - 1000 + } } } @@ -3427,6 +3434,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { confs []uint64 requiredForRemainingSwaps uint64 expectChangeLocked bool + txTimeWithinLimit bool // needed to test AccelerateOrder and AccelerationEstimate expectAccelerateOrderErr bool @@ -3635,6 +3643,18 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { }, }, }, + { + name: "tx time within limit", + txTimeWithinLimit: true, + changeAmount: int64(expectedFees), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: confs, + newFeeRate: 50, + suggestedFeeRate: 30, + expectAccelerationEstimateErr: true, + expectAccelerateOrderErr: true, + expectPreAccelerateErr: true, + }, } for _, test := range tests { @@ -3663,7 +3683,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { addUTXOToNode(utxo.confs, segwit, utxo.amount, node) } - loadTxsIntoNode(txs, test.fees, test.confs, node, t) + loadTxsIntoNode(txs, test.fees, test.confs, node, test.txTimeWithinLimit, t) testAccelerateOrder := func() { change, txID, err := wallet.AccelerateOrder(swapCoins, accelerations, changeCoin, test.requiredForRemainingSwaps, test.newFeeRate) @@ -3832,3 +3852,118 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { testPreAccelerate() } } + +func TestTooEarlyToAcceelrate(t *testing.T) { + tests := []struct { + name string + confirmations []uint64 + isAcceleration []bool + secondsBeforeNow []uint64 + expectTooEarly bool + expectMinTimeToAccelerate int64 // seconds from now +-1 + expectError bool + }{ + { + name: "all confirmed", + confirmations: []uint64{2, 2, 1, 1}, + isAcceleration: []bool{false, true, false, true}, + secondsBeforeNow: []uint64{ + minTimeBeforeAcceleration + 1000, + minTimeBeforeAcceleration + 800, + minTimeBeforeAcceleration + 500, + minTimeBeforeAcceleration + 300, + }, + expectError: true, + }, + { + name: "no accelerations, not too early", + confirmations: []uint64{2, 2, 0, 0}, + isAcceleration: []bool{false, false, false, false}, + secondsBeforeNow: []uint64{ + minTimeBeforeAcceleration + 1000, + minTimeBeforeAcceleration + 800, + minTimeBeforeAcceleration + 500, + minTimeBeforeAcceleration + 300, + }, + expectTooEarly: false, + expectMinTimeToAccelerate: -500, + }, + { + name: "acceleration after unconfirmed, not too early", + confirmations: []uint64{2, 2, 0, 0}, + isAcceleration: []bool{false, false, false, true}, + secondsBeforeNow: []uint64{ + minTimeBeforeAcceleration + 1000, + minTimeBeforeAcceleration + 800, + minTimeBeforeAcceleration + 500, + minTimeBeforeAcceleration + 300, + }, + expectTooEarly: false, + expectMinTimeToAccelerate: -300, + }, + { + name: "no accelerations, too early", + confirmations: []uint64{2, 2, 0, 0}, + isAcceleration: []bool{false, false, false, false}, + secondsBeforeNow: []uint64{ + minTimeBeforeAcceleration + 1000, + minTimeBeforeAcceleration + 800, + minTimeBeforeAcceleration - 300, + minTimeBeforeAcceleration - 500, + }, + expectTooEarly: true, + expectMinTimeToAccelerate: 300, + }, + { + name: "acceleration after unconfirmed, too early", + confirmations: []uint64{2, 2, 0, 0}, + isAcceleration: []bool{false, false, false, true}, + secondsBeforeNow: []uint64{ + minTimeBeforeAcceleration + 1000, + minTimeBeforeAcceleration + 800, + minTimeBeforeAcceleration + 500, + minTimeBeforeAcceleration - 300, + }, + expectTooEarly: true, + expectMinTimeToAccelerate: 300, + }, + } + + for _, test := range tests { + sortedTxChain := make([]*GetTransactionResult, 0, len(test.confirmations)) + accelerationCoins := make([]dex.Bytes, 0, len(test.confirmations)) + now := time.Now().Unix() + for i, confs := range test.confirmations { + var txHash chainhash.Hash + copy(txHash[:], encode.RandomBytes(32)) + if test.isAcceleration[i] { + accelerationCoins = append(accelerationCoins, toCoinID(&txHash, 0)) + } + sortedTxChain = append(sortedTxChain, &GetTransactionResult{ + TxID: txHash.String(), + Confirmations: confs, + Time: uint64(now) - test.secondsBeforeNow[i], + }) + } + tooEarly, minTimeToAccelerate, err := tooEarlyToAccelerate(sortedTxChain, accelerationCoins) + if test.expectError { + if err == nil { + t.Fatalf("%s: expected error but did not get", test.name) + } + continue + } + if err != nil { + t.Fatalf("%s: unexpected error: %v", test.name, err) + } + + if tooEarly != test.expectTooEarly { + t.Fatalf("%s: too early expected: %v, got %v", test.name, test.expectTooEarly, tooEarly) + } + + if minTimeToAccelerate > uint64(now+test.expectMinTimeToAccelerate+1) || + minTimeToAccelerate < uint64(now+test.expectMinTimeToAccelerate-1) { + t.Fatalf("%s: min time to accelerate expected: %v, got %v", + test.name, test.expectMinTimeToAccelerate, minTimeToAccelerate) + } + } +} diff --git a/client/asset/btc/spv_test.go b/client/asset/btc/spv_test.go index 45541e1cb7..ce9cdd0dad 100644 --- a/client/asset/btc/spv_test.go +++ b/client/asset/btc/spv_test.go @@ -251,7 +251,8 @@ func (c *tBtcWallet) walletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetail } return &wtxmgr.TxDetails{ TxRecord: wtxmgr.TxRecord{ - MsgTx: *tx, + MsgTx: *tx, + Received: time.Unix(int64(txData.Time), 0), }, Block: wtxmgr.BlockMeta{ Block: wtxmgr.Block{ diff --git a/client/asset/interface.go b/client/asset/interface.go index 67a57f4dfc..a15a22bf01 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -380,7 +380,9 @@ type Accelerator interface { // have an average fee rate of newFeeRate. requiredForRemainingSwaps is // passed in to ensure that the new change coin will have enough funds to // initiate the additional swaps that will be required to complete the - // order. + // order. + // + // The returned change coin may be nil, and should be checked before use. AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (Coin, string, error) // AccelerationEstimate takes the same parameters as AccelerateOrder, but // instead of broadcasting the acceleration transaction, it just returns diff --git a/client/core/core.go b/client/core/core.go index d7ba4d86c9..919833462f 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -7135,10 +7135,22 @@ func (c *Core) DeleteArchivedRecords(olderThan *time.Time, matchesFile, ordersFi func (c *Core) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (string, error) { _, err := c.encryptionKey(pw) if err != nil { - return "", fmt.Errorf("Accelerate password error: %w", err) + return "", fmt.Errorf("AccelerateOrder password error: %w", err) } - tracker, swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := c.orderAccelerationParameters(oidB) + oid, err := order.IDFromBytes(oidB) + if err != nil { + return "", err + } + + tracker, err := c.findActiveOrder(oid) + if err != nil { + return "", err + } + tracker.mtx.Lock() + defer tracker.mtx.Unlock() + + swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := c.orderAccelerationParameters(tracker) if err != nil { return "", err } @@ -7149,9 +7161,13 @@ func (c *Core) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (st return "", err } - tracker.metaData.ChangeCoin = order.CoinID(newChangeCoin.ID()) + if newChangeCoin != nil { + tracker.metaData.ChangeCoin = order.CoinID(newChangeCoin.ID()) + tracker.coins[newChangeCoin.ID().String()] = newChangeCoin + } else { + tracker.metaData.ChangeCoin = nil + } tracker.metaData.AccelerationCoins = append(tracker.metaData.AccelerationCoins, tracker.metaData.ChangeCoin) - tracker.coins[newChangeCoin.ID().String()] = newChangeCoin err = tracker.db.UpdateOrder(tracker.metaOrder()) if err != nil { c.log.Errorf("AccelerateOrder: failed to update order in database: %v", err) @@ -7163,7 +7179,19 @@ func (c *Core) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (st // AccelerationEstimate returns the amount of funds that would be needed to // accelerate the swap transactions in an order to a desired fee rate. func (c *Core) AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, error) { - tracker, swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, err := c.orderAccelerationParameters(oidB) + oid, err := order.IDFromBytes(oidB) + if err != nil { + return 0, err + } + + tracker, err := c.findActiveOrder(oid) + if err != nil { + return 0, err + } + tracker.mtx.RLock() + defer tracker.mtx.RUnlock() + + swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, err := c.orderAccelerationParameters(tracker) if err != nil { return 0, err } @@ -7179,7 +7207,18 @@ func (c *Core) AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, // PreAccelerateOrder returns information the user can use to decide how much // to accelerate stuck swap transactions in an order. func (c *Core) PreAccelerateOrder(oidB dex.Bytes) (*PreAccelerate, error) { - tracker, swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := c.orderAccelerationParameters(oidB) + oid, err := order.IDFromBytes(oidB) + if err != nil { + return nil, err + } + + tracker, err := c.findActiveOrder(oid) + if err != nil { + return nil, err + } + tracker.mtx.RLock() + defer tracker.mtx.RUnlock() + swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := c.orderAccelerationParameters(tracker) if err != nil { return nil, err } @@ -7199,44 +7238,46 @@ func (c *Core) PreAccelerateOrder(oidB dex.Bytes) (*PreAccelerate, error) { }, nil } +// findActiveOrder will search the dex connections for an active order by order +// id. An error is returned if it cannot be found. +func (c *Core) findActiveOrder(oid order.OrderID) (*trackedTrade, error) { + for _, dc := range c.dexConnections() { + tracker, _, _ := dc.findOrder(oid) + if tracker != nil { + return tracker, nil + } + } + return nil, fmt.Errorf("could not find active order with order id: %s", oid) +} + // orderAccelerationParameters takes an order id, and returns the parameters // needed to accelerate the swap transactions in that order. -func (c *Core) orderAccelerationParameters(oidB dex.Bytes) (tracker *trackedTrade, swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps uint64, err error) { - makeError := func(err error) (*trackedTrade, []dex.Bytes, []dex.Bytes, dex.Bytes, uint64, error) { - return nil, nil, nil, nil, 0, err +func (c *Core) orderAccelerationParameters(tracker *trackedTrade) (swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps uint64, err error) { + makeError := func(err error) ([]dex.Bytes, []dex.Bytes, dex.Bytes, uint64, error) { + return nil, nil, nil, 0, err } - if len(oidB) != order.OrderIDSize { - return makeError(fmt.Errorf("wrong order ID length. wanted %d, got %d", order.OrderIDSize, len(oidB))) + if tracker.metaData.ChangeCoin == nil { + return makeError(fmt.Errorf("order does not have change which can be accelerated")) } - var oid order.OrderID - copy(oid[:], oidB) - for _, dc := range c.dexConnections() { - tracker, _, _ = dc.findOrder(oid) - if tracker != nil { - mktID := marketName(tracker.Base(), tracker.Quote()) - mkt := dc.marketConfig(mktID) - if mkt == nil { - return makeError(fmt.Errorf("could not find market: %v", mktID)) - } - lotSize := mkt.LotSize - fromAsset := dc.assets[tracker.fromAssetID] - if fromAsset == nil { - return makeError(fmt.Errorf("could not find asset with id: %v", tracker.fromAssetID)) - } - swapSize := fromAsset.SwapSize - lotsRemaining := tracker.Trade().Remaining() / lotSize - requiredForRemainingSwaps = lotsRemaining * swapSize * tracker.metaData.MaxFeeRate - break - } - } - if tracker == nil { - return makeError(fmt.Errorf("could not find active order with id: %s", oid)) + if len(tracker.metaData.AccelerationCoins) > 10 { + return makeError(fmt.Errorf("order has already been accelerated too many times")) } - tracker.mtx.Lock() - defer tracker.mtx.Unlock() + mktID := marketName(tracker.Base(), tracker.Quote()) + mkt := tracker.dc.marketConfig(mktID) + if mkt == nil { + return makeError(fmt.Errorf("could not find market: %v", mktID)) + } + lotSize := mkt.LotSize + fromAsset := tracker.dc.assets[tracker.fromAssetID] + if fromAsset == nil { + return makeError(fmt.Errorf("could not find asset with id: %v", tracker.fromAssetID)) + } + swapSize := fromAsset.SwapSize + lotsRemaining := tracker.Trade().Remaining() / lotSize + requiredForRemainingSwaps = lotsRemaining * swapSize * tracker.metaData.MaxFeeRate swapCoins = make([]dex.Bytes, 0, len(tracker.matches)) for _, match := range tracker.matches { @@ -7260,9 +7301,5 @@ func (c *Core) orderAccelerationParameters(oidB dex.Bytes) (tracker *trackedTrad accelerationCoins = append(accelerationCoins, dex.Bytes(coin)) } - if tracker.metaData.ChangeCoin == nil { - return makeError(fmt.Errorf("order does not have change which can be accelerated")) - } - - return tracker, swapCoins, accelerationCoins, dex.Bytes(tracker.metaData.ChangeCoin), requiredForRemainingSwaps, nil + return swapCoins, accelerationCoins, dex.Bytes(tracker.metaData.ChangeCoin), requiredForRemainingSwaps, nil } diff --git a/dex/order/order.go b/dex/order/order.go index 6e8cec20cb..3c1b742f83 100644 --- a/dex/order/order.go +++ b/dex/order/order.go @@ -45,6 +45,16 @@ func IDFromHex(sid string) (OrderID, error) { return oid, nil } +// IDFromHex converts a byte slice to an OrderID. +func IDFromBytes(b []byte) (OrderID, error) { + if len(b) != OrderIDSize { + return OrderID{}, fmt.Errorf("invalid order ID. wanted length %d but got %d", OrderIDSize, len(b)) + } + var oid OrderID + copy(oid[:], b) + return oid, nil +} + // String returns a hexadecimal representation of the OrderID. String implements // fmt.Stringer. func (oid OrderID) String() string { From 4bba6129f2efbc1444b5bf62e58b22d2912c01d3 Mon Sep 17 00:00:00 2001 From: martonp Date: Sat, 16 Apr 2022 18:50:33 +0700 Subject: [PATCH 04/13] Don't sort transactions. --- client/asset/btc/btc.go | 128 +++++++---------------------------- client/asset/btc/btc_test.go | 12 ++-- 2 files changed, 32 insertions(+), 108 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index a1a683102f..bc4fb02784 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -2002,7 +2002,7 @@ func (btc *baseWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.B // since either the earliest unconfirmed transaction in the chain, or the // latest acceleration transaction. It also returns the minimum time when // the user can do an acceleration. -func tooEarlyToAccelerate(sortedTxChain []*GetTransactionResult, accelerationCoins []dex.Bytes) (bool, uint64, error) { +func tooEarlyToAccelerate(txs []*GetTransactionResult, accelerationCoins []dex.Bytes) (bool, uint64, error) { accelerationTxs := make(map[string]bool, len(accelerationCoins)) for _, accelerationCoin := range accelerationCoins { txHash, _, err := decodeCoinID(accelerationCoin) @@ -2012,24 +2012,30 @@ func tooEarlyToAccelerate(sortedTxChain []*GetTransactionResult, accelerationCoi accelerationTxs[txHash.String()] = true } - var timeToCompare uint64 - for _, tx := range sortedTxChain { + var latestAcceleration, earliestUnconfirmed uint64 + for _, tx := range txs { if tx.Confirmations > 0 { continue } - if timeToCompare == 0 { - timeToCompare = tx.Time + if accelerationTxs[tx.TxID] && tx.Time > latestAcceleration { + latestAcceleration = tx.Time continue } - if accelerationTxs[tx.TxID] { - timeToCompare = tx.Time + if earliestUnconfirmed == 0 || tx.Time < earliestUnconfirmed { + earliestUnconfirmed = tx.Time } } - - if timeToCompare == 0 { + if latestAcceleration == 0 && earliestUnconfirmed == 0 { return false, 0, fmt.Errorf("no need to accelerate because all tx are confirmed") } + var timeToCompare uint64 + if latestAcceleration != 0 { + timeToCompare = latestAcceleration + } else { + timeToCompare = earliestUnconfirmed + } + currentTime := uint64(time.Now().Unix()) minAccelerationTime := timeToCompare + minTimeBeforeAcceleration return minAccelerationTime > currentTime, minAccelerationTime, nil @@ -2053,13 +2059,13 @@ func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, c return makeError(err) } - sortedTxChain, err := btc.sortedTxChain(swapCoins, accelerationCoins, changeCoin) + txs, err := btc.getTransactions(append(swapCoins, accelerationCoins...)) if err != nil { return makeError(fmt.Errorf("failed to sort swap chain: %w", err)) } var swapTxsSize, feesAlreadyPaid uint64 - for _, tx := range sortedTxChain { + for _, tx := range txs { if tx.Confirmations > 0 { continue } @@ -2077,7 +2083,7 @@ func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, c } if btc.net != dex.Simnet { - tooEarly, minAccelerationTime, err := tooEarlyToAccelerate(sortedTxChain, accelerationCoins) + tooEarly, minAccelerationTime, err := tooEarlyToAccelerate(txs, accelerationCoins) if err != nil { return makeError(err) } @@ -2183,7 +2189,7 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B return makeError(err) } - sortedTxChain, err := btc.sortedTxChain(swapCoins, accelerationCoins, changeCoin) + txs, err := btc.getTransactions(append(swapCoins, accelerationCoins...)) if err != nil { return makeError(fmt.Errorf("failed to sort swap chain: %w", err)) } @@ -2193,7 +2199,7 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B return makeError(err) } - additionalFeesRequired, err := btc.additionalFeesRequired(sortedTxChain, newFeeRate) + additionalFeesRequired, err := btc.additionalFeesRequired(txs, newFeeRate) if err != nil { return makeError(err) } @@ -2203,7 +2209,7 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B } if btc.net != dex.Simnet { - tooEarly, minAccelerationTime, err := tooEarlyToAccelerate(sortedTxChain, accelerationCoins) + tooEarly, minAccelerationTime, err := tooEarlyToAccelerate(txs, accelerationCoins) if err != nil { return makeError(err) } @@ -2342,29 +2348,14 @@ func (btc *baseWallet) changeCanBeAccelerated(changeTxHash *chainhash.Hash, chan return nil } -// sortedTxChain takes a list of swap coins, acceleration tx IDs, and a change -// coin, and returns a sorted list of transactions. An error is returned if a -// sorted list of transactions that ends with a transaction containing the change -// cannot be created using each of the swap coins and acceleration transactions. -func (btc *baseWallet) sortedTxChain(swapCoins, accelerationCoins []dex.Bytes, change dex.Bytes) ([]*GetTransactionResult, error) { - txChain := make([]*GetTransactionResult, 0, len(swapCoins)+len(accelerationCoins)) - if len(swapCoins) == 0 { +// getTransactions retrieves the transactions that created coins. +func (btc *baseWallet) getTransactions(coins []dex.Bytes) ([]*GetTransactionResult, error) { + txChain := make([]*GetTransactionResult, 0, len(coins)) + if len(coins) == 0 { return txChain, nil } - for _, coinID := range swapCoins { - txHash, _, err := decodeCoinID(coinID) - if err != nil { - return nil, err - } - getTxRes, err := btc.node.getWalletTransaction(txHash) - if err != nil { - return nil, err - } - txChain = append(txChain, getTxRes) - } - - for _, coinID := range accelerationCoins { + for _, coinID := range coins { txHash, _, err := decodeCoinID(coinID) if err != nil { return nil, err @@ -2376,73 +2367,6 @@ func (btc *baseWallet) sortedTxChain(swapCoins, accelerationCoins []dex.Bytes, c txChain = append(txChain, getTxRes) } - msgTxs := make(map[string]*wire.MsgTx, len(txChain)) - for _, gtr := range txChain { - msgTx, err := msgTxFromBytes(gtr.Hex) - if err != nil { - return nil, err - } - msgTxs[gtr.TxID] = msgTx - } - - changeTxHash, _, err := decodeCoinID(change) - if err != nil { - return nil, err - } - - swap := func(i, j int) { - temp := txChain[j] - txChain[j] = txChain[i] - txChain[i] = temp - } - - // sourcesOfTxInputs finds the transaction IDs that the inputs - // for a certain transaction come from. If all the transactions - // are from the same order, only the first transaction in - // the chain can have multiple inputs. - sourcesOfTxInputs := func(txID string) (map[string]bool, error) { - lastTx, found := msgTxs[txID] - if !found { - // this should never happen - return nil, fmt.Errorf("could not find tx with id: %v", txID) - } - - inputSources := make(map[string]bool, len(lastTx.TxIn)) - for _, in := range lastTx.TxIn { - inputSources[in.PreviousOutPoint.Hash.String()] = true - } - return inputSources, nil - } - - // The last tx in the chain must have the same tx hash as the change. - for i, tx := range txChain { - if tx.TxID == changeTxHash.String() { - swap(i, len(txChain)-1) - break - } - if i == len(txChain)-1 { - return nil, fmt.Errorf("could not find tx containing change coin") - } - } - - // We work backwards to find each element of the swap chain. - for i := len(txChain) - 2; i >= 0; i-- { - lastTxInputs, err := sourcesOfTxInputs(txChain[i+1].TxID) - if err != nil { - return nil, err - } - - for j, getTxRes := range txChain { - if lastTxInputs[getTxRes.TxID] { - swap(i, j) - break - } - if j == len(txChain)-1 { - return nil, errors.New("could not find previous element of sorted chain") - } - } - } - return txChain, nil } diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index 27cf5d1de6..62237e2e1d 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -3853,7 +3853,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { } } -func TestTooEarlyToAcceelrate(t *testing.T) { +func TestTooEarlyToAccelerate(t *testing.T) { tests := []struct { name string confirmations []uint64 @@ -3877,26 +3877,26 @@ func TestTooEarlyToAcceelrate(t *testing.T) { }, { name: "no accelerations, not too early", - confirmations: []uint64{2, 2, 0, 0}, + confirmations: []uint64{2, 0, 0, 2}, isAcceleration: []bool{false, false, false, false}, secondsBeforeNow: []uint64{ minTimeBeforeAcceleration + 1000, - minTimeBeforeAcceleration + 800, minTimeBeforeAcceleration + 500, minTimeBeforeAcceleration + 300, + minTimeBeforeAcceleration + 800, }, expectTooEarly: false, expectMinTimeToAccelerate: -500, }, { name: "acceleration after unconfirmed, not too early", - confirmations: []uint64{2, 2, 0, 0}, - isAcceleration: []bool{false, false, false, true}, + confirmations: []uint64{0, 2, 2, 0}, + isAcceleration: []bool{true, false, false, false}, secondsBeforeNow: []uint64{ + minTimeBeforeAcceleration + 300, minTimeBeforeAcceleration + 1000, minTimeBeforeAcceleration + 800, minTimeBeforeAcceleration + 500, - minTimeBeforeAcceleration + 300, }, expectTooEarly: false, expectMinTimeToAccelerate: -300, From 6b7770aadb44d69961c6f436da27f8d77777c71e Mon Sep 17 00:00:00 2001 From: martonp Date: Sat, 16 Apr 2022 20:57:02 +0700 Subject: [PATCH 05/13] Update bitcoin block explorer to mempool.space. --- client/webserver/site/src/js/order.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/webserver/site/src/js/order.ts b/client/webserver/site/src/js/order.ts index 1b94838a29..3c1380eda8 100644 --- a/client/webserver/site/src/js/order.ts +++ b/client/webserver/site/src/js/order.ts @@ -408,8 +408,8 @@ const CoinExplorers: Record string>> = { } }, 0: { // btc - [Mainnet]: (cid: string) => `https://bitaps.com/${cid.split(':')[0]}`, - [Testnet]: (cid: string) => `https://tbtc.bitaps.com/${cid.split(':')[0]}` + [Mainnet]: (cid: string) => `https://mempool.space/tx/${cid.split(':')[0]}`, + [Testnet]: (cid: string) => `https://mempool.space/testnet/tx/${cid.split(':')[0]}` }, 2: { // ltc [Mainnet]: (cid: string) => `https://ltc.bitaps.com/${cid.split(':')[0]}`, From 053f150ca6bad5d3783e53aa9b88bba5e039ff95 Mon Sep 17 00:00:00 2001 From: martonp Date: Tue, 26 Apr 2022 10:03:56 +0700 Subject: [PATCH 06/13] Updates based on chappjc review --- client/asset/btc/btc.go | 337 +++++----- client/asset/btc/rpcclient.go | 2 +- client/asset/btc/spv.go | 4 +- client/asset/btc/spv_test.go | 2 +- client/core/core.go | 109 +-- client/core/core_test.go | 628 +++++++++++++++--- client/core/trade.go | 87 +++ client/webserver/api.go | 3 +- client/webserver/locales/en-us.go | 6 +- client/webserver/site/src/html/order.tmpl | 2 +- .../site/src/localized_html/en-US/order.tmpl | 6 +- .../site/src/localized_html/pl-PL/order.tmpl | 6 +- .../site/src/localized_html/pt-BR/order.tmpl | 6 +- .../site/src/localized_html/zh-CN/order.tmpl | 6 +- dex/networks/btc/script.go | 14 +- dex/order/order.go | 2 +- 16 files changed, 842 insertions(+), 378 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index bc4fb02784..a1aa1048ae 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -61,7 +61,7 @@ const ( // splitTxBaggageSegwit it the analogue of splitTxBaggage for segwit. // We include the 2 bytes for marker and flag. splitTxBaggageSegwit = dexbtc.MinimumTxOverhead + 2*dexbtc.P2WPKHOutputSize + - dexbtc.RedeemP2PWKHInputTotalSize + dexbtc.RedeemP2WPKHInputSize + ((dexbtc.RedeemP2WPKHInputWitnessWeight + dexbtc.SegwitMarkerAndFlagWeight + 3) / 4) walletTypeLegacy = "" walletTypeRPC = "bitcoindRPC" @@ -1470,7 +1470,6 @@ func (btc *baseWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, er // FundOrder selects coins for use in an order. The coins will be locked, and // will not be returned in subsequent calls to FundOrder or calculated in calls // to Available, unless they are unlocked with ReturnCoins. -// The returned []dex.Bytes contains the redeem scripts for the selected coins. // Equal number of coins and redeemed scripts must be returned. A nil or empty // dex.Bytes should be appended to the redeem scripts collection for coins with // no redeem script. @@ -1763,7 +1762,7 @@ func (btc *baseWallet) split(value uint64, lots uint64, outputs []*output, func (btc *baseWallet) splitBaggageFees(maxFeeRate uint64) (swapInputSize, baggage uint64) { if btc.segwit { baggage = maxFeeRate * splitTxBaggageSegwit - swapInputSize = dexbtc.RedeemP2PWKHInputTotalSize + swapInputSize = dexbtc.RedeemP2WPKHInputTotalSize return } baggage = maxFeeRate * splitTxBaggage @@ -1930,6 +1929,123 @@ func (btc *baseWallet) fundedTx(coins asset.Coins) (*wire.MsgTx, uint64, []outPo return baseTx, totalIn, pts, nil } +// signedAccelerationTx returns a signed transaction that sends funds to a +// change address controlled by this wallet. This new transaction will have +// a fee high enough to make the average fee of the unmined swapCoins and +// accelerationTxs to be newFeeRate. +func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (*wire.MsgTx, *output, uint64, error) { + makeError := func(err error) (*wire.MsgTx, *output, uint64, error) { + return nil, nil, 0, err + } + + changeTxHash, changeVout, err := decodeCoinID(changeCoin) + if err != nil { + return makeError(err) + } + + err = btc.changeCanBeAccelerated(changeTxHash, changeVout, requiredForRemainingSwaps) + if err != nil { + return makeError(err) + } + + txs, err := btc.getTransactions(append(swapCoins, accelerationCoins...)) + if err != nil { + return makeError(fmt.Errorf("failed to sort swap chain: %w", err)) + } + + changeOutput, err := btc.lookupOutput(changeTxHash, changeVout) + if err != nil { + return makeError(err) + } + + additionalFeesRequired, err := btc.additionalFeesRequired(txs, newFeeRate) + if err != nil { + return makeError(err) + } + + if additionalFeesRequired <= 0 { + return makeError(fmt.Errorf("no additional fees are required to move the fee rate to %v", newFeeRate)) + } + + if btc.net != dex.Simnet { + tooEarly, minAccelerationTime, err := tooEarlyToAccelerate(txs, accelerationCoins) + if err != nil { + return makeError(err) + } + if tooEarly { + minLocalTime := time.Unix(int64(minAccelerationTime), 0).Local() + return makeError(fmt.Errorf("cannot accelerate until %v", minLocalTime)) + } + } + + txSize := uint64(dexbtc.MinimumTxOverhead) + if btc.segwit { + txSize += dexbtc.RedeemP2WPKHInputTotalSize + } else { + txSize += dexbtc.RedeemP2PKHInputSize + } + if requiredForRemainingSwaps > 0 { + if btc.segwit { + txSize += dexbtc.P2WPKHOutputSize + } else { + txSize += dexbtc.P2PKHOutputSize + } + } + + fundsRequired := additionalFeesRequired + requiredForRemainingSwaps + txSize*newFeeRate + + var additionalInputs asset.Coins + if fundsRequired > changeOutput.value { + // If change not enough, need to use other UTXOs. + utxos, _, _, err := btc.spendableUTXOs(1) + if err != nil { + return makeError(err) + } + + _, _, additionalInputs, _, _, _, err = fund(utxos, func(inputSize, inputsVal uint64) bool { + txSize := dexbtc.MinimumTxOverhead + inputSize + + // input is the change input that we must use + if btc.segwit { + txSize += dexbtc.RedeemP2WPKHInputTotalSize + } else { + txSize += dexbtc.RedeemP2PKHInputSize + } + + if requiredForRemainingSwaps > 0 { + if btc.segwit { + txSize += dexbtc.P2WPKHOutputSize + } else { + txSize += dexbtc.P2PKHOutputSize + } + } + + totalFees := additionalFeesRequired + txSize*newFeeRate + return totalFees+requiredForRemainingSwaps <= inputsVal+changeOutput.value + }) + if err != nil { + return makeError(fmt.Errorf("failed to fund acceleration tx: %w", err)) + } + } + + baseTx, totalIn, _, err := btc.fundedTx(append(additionalInputs, changeOutput)) + if err != nil { + return makeError(err) + } + + changeAddr, err := btc.node.changeAddress() + if err != nil { + return makeError(fmt.Errorf("error creating change address: %w", err)) + } + + tx, output, txFee, err := btc.signTxAndAddChange(baseTx, changeAddr, totalIn, additionalFeesRequired, newFeeRate) + if err != nil { + return makeError(err) + } + + return tx, output, txFee + additionalFeesRequired, nil +} + // AccelerateOrder uses the Child-Pays-For-Parent technique to accelerate a // chain of swap transactions and previous accelerations. It broadcasts a new // transaction with a fee high enough so that the average fee of all the @@ -1943,6 +2059,11 @@ func (btc *baseWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, btc.fundingMtx.Lock() defer btc.fundingMtx.Unlock() + changeTxHash, changeVout, err := decodeCoinID(changeCoin) + if err != nil { + return nil, "", err + } + signedTx, newChange, _, err := btc.signedAccelerationTx(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) if err != nil { return nil, "", err @@ -1953,34 +2074,38 @@ func (btc *baseWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, return nil, "", err } - var newChangeCoin asset.Coin - if newChange != nil { - newChangeCoin = newChange + // Delete the old change from the cache + delete(btc.fundingCoins, newOutPoint(changeTxHash, changeVout)) - // Checking required for remaining swaps > 0 because this ensures if - // the previous change was locked, this one will also be locked. If - // requiredForRemainingSwaps = 0, but the change was locked, - // signedAccelerationTx would have returned an error since this means - // that the change was locked by another order. - if requiredForRemainingSwaps > 0 { - err = btc.node.lockUnspent(false, []*output{newChange}) - if err != nil { - // The transaction is already broadcasted, so don't fail now. - btc.log.Errorf("failed to lock change output: %v", err) - } + if newChange == nil { + return nil, signedTx.TxHash().String(), nil + } - // Log it as a fundingCoin, since it is expected that this will be - // chained into further matches. - btc.fundingCoins[newChange.pt] = &utxo{ - txHash: newChange.txHash(), - vout: newChange.vout(), - address: newChange.String(), - amount: newChange.value, - } + // Checking required for remaining swaps > 0 because this ensures if + // the previous change was locked, this one will also be locked. If + // requiredForRemainingSwaps = 0, but the change was locked, + // signedAccelerationTx would have returned an error since this means + // that the change was locked by another order. + if requiredForRemainingSwaps > 0 { + err = btc.node.lockUnspent(false, []*output{newChange}) + if err != nil { + // The transaction is already broadcasted, so don't fail now. + btc.log.Errorf("failed to lock change output: %v", err) + } + + // Log it as a fundingCoin, since it is expected that this will be + // chained into further matches. + btc.fundingCoins[newChange.pt] = &utxo{ + txHash: newChange.txHash(), + vout: newChange.vout(), + address: newChange.String(), + amount: newChange.value, } } - return newChangeCoin, signedTx.TxHash().String(), err + // return nil error since tx is already broadcast, and core needs to update + // the change coin + return newChange, signedTx.TxHash().String(), nil } // AccelerationEstimate takes the same parameters as AccelerateOrder, but @@ -2044,7 +2169,12 @@ func tooEarlyToAccelerate(txs []*GetTransactionResult, accelerationCoins []dex.B // PreAccelerate returns the current average fee rate of the unmined swap // initiation and acceleration transactions, and also returns a suggested // range that the fee rate should be increased to in order to expedite mining. -func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange asset.XYRange, err error) { +// The feeSuggestion argument is the current prevailing network rate. It is +// used to help determine the suggestedRange, which is a range meant to give +// the user a good amount of flexibility in determining the post acceleration +// effective fee rate, but still not allowing them to pick something +// outrageously high. +func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentEffectiveRate uint64, suggestedRange asset.XYRange, err error) { makeError := func(err error) (uint64, asset.XYRange, error) { return 0, asset.XYRange{}, err } @@ -2138,152 +2268,45 @@ func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, c } maxRate := (changeOutput.value + feesAlreadyPaid + utxosVal - requiredForRemainingSwaps) / (newChangeTxSize + swapTxsSize) - currentRate = feesAlreadyPaid / swapTxsSize - - if maxRate <= currentRate { - return makeError(fmt.Errorf("cannot accelerate, max rate %v <= current rate %v", maxRate, currentRate)) - } - - maxSuggestion := currentRate * 5 - if feeSuggestion*5 > maxSuggestion { - maxSuggestion = feeSuggestion * 5 + currentEffectiveRate = feesAlreadyPaid / swapTxsSize + + if maxRate <= currentEffectiveRate { + return makeError(fmt.Errorf("cannot accelerate, max rate %v <= current rate %v", maxRate, currentEffectiveRate)) + } + + // The suggested range will be the min and max of the slider that is + // displayed on the UI. The minimum of the range is 1 higher than the + // current effective range of the swap transactions. The max of the range + // will be the maximum of 5x the current effective rate, or 5x the current + // prevailing network rate. This is a completely arbitrary choice, but in + // this way the user will definitely be able to accelerate at least 5x the + // original rate, and even if the prevailing network rate is much higher + // than the current effective rate, they will still have a comformtable + // buffer above the prevailing network rate. + const scalingFactor = 5 + maxSuggestion := currentEffectiveRate * scalingFactor + if feeSuggestion > currentEffectiveRate { + maxSuggestion = feeSuggestion * scalingFactor } if maxRate < maxSuggestion { maxSuggestion = maxRate } - suggestedRange = asset.XYRange{ Start: asset.XYRangePoint{ Label: "Min", - X: float64(currentRate+1) / float64(currentRate), - Y: float64(currentRate + 1), + X: float64(currentEffectiveRate+1) / float64(currentEffectiveRate), + Y: float64(currentEffectiveRate + 1), }, End: asset.XYRangePoint{ Label: "Max", - X: float64(maxSuggestion) / float64(currentRate), + X: float64(maxSuggestion) / float64(currentEffectiveRate), Y: float64(maxSuggestion), }, XUnit: "X", YUnit: btc.walletInfo.UnitInfo.AtomicUnit + "/" + btc.sizeUnit(), } - return currentRate, suggestedRange, nil -} - -// signedAccelerationTx returns a signed transaction that sends funds to a -// change address controlled by this wallet. This new transaction will have -// a fee high enough to make the average fee of the unmined swapCoins and -// accelerationTxs to be newFeeRate. -func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (*wire.MsgTx, *output, uint64, error) { - makeError := func(err error) (*wire.MsgTx, *output, uint64, error) { - return nil, nil, 0, err - } - - changeTxHash, changeVout, err := decodeCoinID(changeCoin) - if err != nil { - return makeError(err) - } - - err = btc.changeCanBeAccelerated(changeTxHash, changeVout, requiredForRemainingSwaps) - if err != nil { - return makeError(err) - } - - txs, err := btc.getTransactions(append(swapCoins, accelerationCoins...)) - if err != nil { - return makeError(fmt.Errorf("failed to sort swap chain: %w", err)) - } - - changeOutput, err := btc.lookupOutput(changeTxHash, changeVout) - if err != nil { - return makeError(err) - } - - additionalFeesRequired, err := btc.additionalFeesRequired(txs, newFeeRate) - if err != nil { - return makeError(err) - } - - if additionalFeesRequired <= 0 { - return makeError(fmt.Errorf("no additional fees are required to move the fee rate to %v", newFeeRate)) - } - - if btc.net != dex.Simnet { - tooEarly, minAccelerationTime, err := tooEarlyToAccelerate(txs, accelerationCoins) - if err != nil { - return makeError(err) - } - if tooEarly { - minLocalTime := time.Unix(int64(minAccelerationTime), 0).Local() - return makeError(fmt.Errorf("cannot accelerate until %v", minLocalTime)) - } - } - - txSize := dexbtc.MinimumTxOverhead - if btc.segwit { - txSize += dexbtc.RedeemP2PWKHInputTotalSize - } else { - txSize += dexbtc.RedeemP2PKHInputSize - } - if requiredForRemainingSwaps > 0 { - if btc.segwit { - txSize += dexbtc.P2WPKHOutputSize - } else { - txSize += dexbtc.P2PKHOutputSize - } - } - fundsRequired := additionalFeesRequired + requiredForRemainingSwaps + uint64(txSize)*newFeeRate - - var additionalInputs asset.Coins - if fundsRequired > changeOutput.value { - // If change not enough, need to use other UTXOs. - utxos, _, _, err := btc.spendableUTXOs(1) - if err != nil { - return makeError(err) - } - - _, _, additionalInputs, _, _, _, err = fund(utxos, func(inputSize, inputsVal uint64) bool { - txSize := dexbtc.MinimumTxOverhead + inputSize - - // input is the change input that we must use - if btc.segwit { - txSize += dexbtc.RedeemP2PWKHInputTotalSize - } else { - txSize += dexbtc.RedeemP2PKHInputSize - } - - if requiredForRemainingSwaps > 0 { - if btc.segwit { - txSize += dexbtc.P2WPKHOutputSize - } else { - txSize += dexbtc.P2PKHOutputSize - } - } - - totalFees := additionalFeesRequired + txSize*newFeeRate - return totalFees+requiredForRemainingSwaps <= inputsVal+changeOutput.value - }) - if err != nil { - return makeError(fmt.Errorf("failed to fund acceleration tx: %w", err)) - } - } - - baseTx, totalIn, _, err := btc.fundedTx(append(additionalInputs, changeOutput)) - if err != nil { - return makeError(err) - } - - changeAddr, err := btc.node.changeAddress() - if err != nil { - return makeError(fmt.Errorf("error creating change address: %w", err)) - } - - tx, output, txFee, err := btc.signTxAndAddChange(baseTx, changeAddr, totalIn, additionalFeesRequired, newFeeRate) - if err != nil { - return makeError(err) - } - - return tx, output, txFee + additionalFeesRequired, nil + return currentEffectiveRate, suggestedRange, nil } // lookupOutput looks up the value of a transaction output and creates an diff --git a/client/asset/btc/rpcclient.go b/client/asset/btc/rpcclient.go index d106875b83..8e827b17b2 100644 --- a/client/asset/btc/rpcclient.go +++ b/client/asset/btc/rpcclient.go @@ -704,7 +704,7 @@ func (wc *rpcClient) call(method string, args anylist, thing interface{}) error } // serializeMsgTx serializes the wire.MsgTx. -func serializeMsgTx(msgTx *wire.MsgTx) (dex.Bytes, error) { +func serializeMsgTx(msgTx *wire.MsgTx) ([]byte, error) { buf := bytes.NewBuffer(make([]byte, 0, msgTx.SerializeSize())) err := msgTx.Serialize(buf) if err != nil { diff --git a/client/asset/btc/spv.go b/client/asset/btc/spv.go index a477bea67b..cfe619d53f 100644 --- a/client/asset/btc/spv.go +++ b/client/asset/btc/spv.go @@ -1799,14 +1799,14 @@ func (w *spvWallet) getTransaction(txHash *chainhash.Hash) (*GetTransactionResul // TODO: The serialized transaction is already in the DB, so // reserializing can be avoided here. - txBuf, err := serializeMsgTx(&details.MsgTx) + txRaw, err := serializeMsgTx(&details.MsgTx) if err != nil { return nil, err } ret := &GetTransactionResult{ TxID: txHash.String(), - Hex: txBuf, // 'Hex' field name is a lie, kinda + Hex: txRaw, // 'Hex' field name is a lie, kinda Time: uint64(details.Received.Unix()), TimeReceived: uint64(details.Received.Unix()), } diff --git a/client/asset/btc/spv_test.go b/client/asset/btc/spv_test.go index ce9cdd0dad..f144d909f5 100644 --- a/client/asset/btc/spv_test.go +++ b/client/asset/btc/spv_test.go @@ -669,7 +669,7 @@ func TestSendWithSubtract(t *testing.T) { const availableFunds = 5e8 const feeRate = 100 - const inputSize = dexbtc.RedeemP2PWKHInputTotalSize + const inputSize = dexbtc.RedeemP2WPKHInputTotalSize const feesWithChange = (dexbtc.MinimumTxOverhead + 2*dexbtc.P2WPKHOutputSize + inputSize) * feeRate const feesWithoutChange = (dexbtc.MinimumTxOverhead + dexbtc.P2WPKHOutputSize + inputSize) * feeRate diff --git a/client/core/core.go b/client/core/core.go index 919833462f..903782f03b 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -3473,11 +3473,10 @@ func (c *Core) coreOrderFromMetaOrder(mOrd *db.MetaOrder) (*Order, error) { // Order fetches a single user order. func (c *Core) Order(oidB dex.Bytes) (*Order, error) { - if len(oidB) != order.OrderIDSize { - return nil, fmt.Errorf("wrong oid string length. wanted %d, got %d", order.OrderIDSize, len(oidB)) + oid, err := order.IDFromBytes(oidB) + if err != nil { + return nil, err } - var oid order.OrderID - copy(oid[:], oidB) // See if its an active order first. var tracker *trackedTrade for _, dc := range c.dexConnections() { @@ -4535,11 +4534,10 @@ func (c *Core) Cancel(pw []byte, oidB dex.Bytes) error { return fmt.Errorf("Cancel password error: %w", err) } - if len(oidB) != order.OrderIDSize { - return fmt.Errorf("wrong order ID length. wanted %d, got %d", order.OrderIDSize, len(oidB)) + oid, err := order.IDFromBytes(oidB) + if err != nil { + return err } - var oid order.OrderID - copy(oid[:], oidB) for _, dc := range c.dexConnections() { found, err := c.tryCancel(dc, oid) @@ -6145,13 +6143,11 @@ func handlePreimageRequest(c *Core, dc *dexConnection, msg *msgjson.Message) err return fmt.Errorf("preimage request parsing error: %w", err) } - if len(req.OrderID) != order.OrderIDSize { - return fmt.Errorf("invalid order ID in preimage request") + oid, err := order.IDFromBytes(req.OrderID) + if err != nil { + return err } - var oid order.OrderID - copy(oid[:], req.OrderID) - // NEW protocol with commitment specified. if len(req.Commitment) == order.CommitmentSize { // See if we recognize that commitment, and if we do, just wait for the @@ -6791,12 +6787,10 @@ func validateOrderResponse(dc *dexConnection, result *msgjson.OrderResult, ord o return fmt.Errorf("signature error. order abandoned") } ord.SetTime(encode.UnixTimeMilli(int64(result.ServerTime))) - // Check the order ID - if len(result.OrderID) != order.OrderIDSize { - return fmt.Errorf("failed ID length check. order abandoned") + checkID, err := order.IDFromBytes(result.OrderID) + if err != nil { + return err } - var checkID order.OrderID - copy(checkID[:], result.OrderID) oid := ord.ID() if oid != checkID { return fmt.Errorf("failed ID match. order abandoned") @@ -7147,10 +7141,16 @@ func (c *Core) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (st if err != nil { return "", err } + + if !tracker.wallets.fromWallet.traits.IsAccelerator() { + return "", fmt.Errorf("the %s wallet is not an accelerator", + tracker.wallets.fromAsset.Symbol) + } + tracker.mtx.Lock() defer tracker.mtx.Unlock() - swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := c.orderAccelerationParameters(tracker) + swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := tracker.orderAccelerationParameters() if err != nil { return "", err } @@ -7160,7 +7160,6 @@ func (c *Core) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (st if err != nil { return "", err } - if newChangeCoin != nil { tracker.metaData.ChangeCoin = order.CoinID(newChangeCoin.ID()) tracker.coins[newChangeCoin.ID().String()] = newChangeCoin @@ -7168,12 +7167,7 @@ func (c *Core) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (st tracker.metaData.ChangeCoin = nil } tracker.metaData.AccelerationCoins = append(tracker.metaData.AccelerationCoins, tracker.metaData.ChangeCoin) - err = tracker.db.UpdateOrder(tracker.metaOrder()) - if err != nil { - c.log.Errorf("AccelerateOrder: failed to update order in database: %v", err) - } - - return txID, nil + return txID, tracker.db.UpdateOrderMetaData(oid, tracker.metaData) } // AccelerationEstimate returns the amount of funds that would be needed to @@ -7191,7 +7185,7 @@ func (c *Core) AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, tracker.mtx.RLock() defer tracker.mtx.RUnlock() - swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, err := c.orderAccelerationParameters(tracker) + swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, err := tracker.orderAccelerationParameters() if err != nil { return 0, err } @@ -7216,15 +7210,16 @@ func (c *Core) PreAccelerateOrder(oidB dex.Bytes) (*PreAccelerate, error) { if err != nil { return nil, err } + + feeSuggestion := c.feeSuggestionAny(tracker.fromAssetID) + tracker.mtx.RLock() defer tracker.mtx.RUnlock() - swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := c.orderAccelerationParameters(tracker) + swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := tracker.orderAccelerationParameters() if err != nil { return nil, err } - feeSuggestion := c.feeSuggestionAny(tracker.fromAssetID) - currentRate, suggestedRange, err := tracker.wallets.fromWallet.preAccelerate(swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, feeSuggestion) if err != nil { @@ -7249,57 +7244,3 @@ func (c *Core) findActiveOrder(oid order.OrderID) (*trackedTrade, error) { } return nil, fmt.Errorf("could not find active order with order id: %s", oid) } - -// orderAccelerationParameters takes an order id, and returns the parameters -// needed to accelerate the swap transactions in that order. -func (c *Core) orderAccelerationParameters(tracker *trackedTrade) (swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps uint64, err error) { - makeError := func(err error) ([]dex.Bytes, []dex.Bytes, dex.Bytes, uint64, error) { - return nil, nil, nil, 0, err - } - - if tracker.metaData.ChangeCoin == nil { - return makeError(fmt.Errorf("order does not have change which can be accelerated")) - } - - if len(tracker.metaData.AccelerationCoins) > 10 { - return makeError(fmt.Errorf("order has already been accelerated too many times")) - } - - mktID := marketName(tracker.Base(), tracker.Quote()) - mkt := tracker.dc.marketConfig(mktID) - if mkt == nil { - return makeError(fmt.Errorf("could not find market: %v", mktID)) - } - lotSize := mkt.LotSize - fromAsset := tracker.dc.assets[tracker.fromAssetID] - if fromAsset == nil { - return makeError(fmt.Errorf("could not find asset with id: %v", tracker.fromAssetID)) - } - swapSize := fromAsset.SwapSize - lotsRemaining := tracker.Trade().Remaining() / lotSize - requiredForRemainingSwaps = lotsRemaining * swapSize * tracker.metaData.MaxFeeRate - - swapCoins = make([]dex.Bytes, 0, len(tracker.matches)) - for _, match := range tracker.matches { - if match.Status < order.MakerSwapCast { - continue - } - var swapCoinID order.CoinID - if match.Side == order.Maker { - swapCoinID = match.MetaData.Proof.MakerSwap - } else { - if match.Status < order.TakerSwapCast { - continue - } - swapCoinID = match.MetaData.Proof.TakerSwap - } - swapCoins = append(swapCoins, dex.Bytes(swapCoinID)) - } - - accelerationCoins = make([]dex.Bytes, 0, len(tracker.metaData.AccelerationCoins)) - for _, coin := range tracker.metaData.AccelerationCoins { - accelerationCoins = append(accelerationCoins, dex.Bytes(coin)) - } - - return swapCoins, accelerationCoins, dex.Bytes(tracker.metaData.ChangeCoin), requiredForRemainingSwaps, nil -} diff --git a/client/core/core_test.go b/client/core/core_test.go index c27e884a64..80f50dd259 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -13,6 +13,8 @@ import ( "fmt" "math/rand" "os" + "reflect" + "sort" "strconv" "strings" "sync" @@ -646,9 +648,12 @@ type TXCWallet struct { newFeeRate uint64 requiredForRemainingSwaps uint64 } - newAccelerationTxID string - newChangeCoinID dex.Bytes - accelerateOrderErr error + newAccelerationTxID string + newChangeCoinID *dex.Bytes + preAccelerateSwapRate uint64 + preAccelerateSuggestedRange asset.XYRange + accelerationEstimate uint64 + accelerateOrderErr error } var _ asset.Accelerator = (*TXCWallet)(nil) @@ -891,15 +896,57 @@ func (w *TXCWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, ch requiredForRemainingSwaps: requiredForRemainingSwaps, newFeeRate: newFeeRate, } - return &tCoin{id: w.newChangeCoinID}, w.newAccelerationTxID, nil + if w.newChangeCoinID != nil { + return &tCoin{id: *w.newChangeCoinID}, w.newAccelerationTxID, nil + } + + return nil, w.newAccelerationTxID, nil } func (w *TXCWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange asset.XYRange, err error) { - return 0, asset.XYRange{}, nil + if w.accelerateOrderErr != nil { + return 0, asset.XYRange{}, w.accelerateOrderErr + } + + w.accelerationParams = &struct { + swapCoins []dex.Bytes + accelerationCoins []dex.Bytes + changeCoin dex.Bytes + feeSuggestion uint64 + newFeeRate uint64 + requiredForRemainingSwaps uint64 + }{ + swapCoins: swapCoins, + accelerationCoins: accelerationCoins, + changeCoin: changeCoin, + requiredForRemainingSwaps: requiredForRemainingSwaps, + feeSuggestion: feeSuggestion, + } + + return w.preAccelerateSwapRate, w.preAccelerateSuggestedRange, nil } -func (w *TXCWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, error) { - return 0, nil +func (w *TXCWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) { + if w.accelerateOrderErr != nil { + return 0, w.accelerateOrderErr + } + + w.accelerationParams = &struct { + swapCoins []dex.Bytes + accelerationCoins []dex.Bytes + changeCoin dex.Bytes + feeSuggestion uint64 + newFeeRate uint64 + requiredForRemainingSwaps uint64 + }{ + swapCoins: swapCoins, + accelerationCoins: accelerationCoins, + changeCoin: changeCoin, + requiredForRemainingSwaps: requiredForRemainingSwaps, + newFeeRate: newFeeRate, + } + + return w.accelerationEstimate, nil } type TAccountLocker struct { @@ -6061,7 +6108,7 @@ func makeLimitOrder(dc *dexConnection, sell bool, qty, rate uint64) (*order.Limi Quantity: qty, Address: addr, }, - Rate: dcrBtcRateStep, + Rate: rate, Force: order.ImmediateTiF, } dbOrder := &db.MetaOrder{ @@ -7096,133 +7143,494 @@ func TestAccelerateOrder(t *testing.T) { tCore.wallets[tUTXOAssetA.ID] = dcrWallet btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) tCore.wallets[tUTXOAssetB.ID] = btcWallet - walletSet, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, false) - qty := 3 * dcrBtcLotSize + buyWalletSet, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, false) + sellWalletSet, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, false) - lo, dbOrder, preImg, addr := makeLimitOrder(dc, false, qty, dcrBtcRateStep*10) - dbOrder.MetaData.Status = order.OrderStatusExecuted // so there is no order_status request for this - oid := lo.ID() - trade := newTrackedTrade(dbOrder, preImg, dc, mkt.EpochLen, rig.core.lockTimeTaker, rig.core.lockTimeMaker, - rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails, nil, 0, 0) + var newBaseFeeRate uint64 = 55 + var newQuoteFeeRate uint64 = 65 + feeRateSource := func(msg *msgjson.Message, f msgFunc) error { + var resp *msgjson.Message + if string(msg.Payload) == "42" { + resp, _ = msgjson.NewResponse(msg.ID, newBaseFeeRate, nil) + } else { + resp, _ = msgjson.NewResponse(msg.ID, newQuoteFeeRate, nil) + } + f(resp) + return nil + } - dc.trades[trade.ID()] = trade + type testMatch struct { + status order.MatchStatus + quantity uint64 + rate uint64 + side order.MatchSide + } - match1ID := ordertest.RandomMatchID() - match1 := &matchTracker{ - MetaMatch: db.MetaMatch{ - MetaData: &db.MatchMetaData{ - Proof: db.MatchProof{ - MakerSwap: encode.RandomBytes(32), - TakerSwap: encode.RandomBytes(32), + tests := []struct { + name string + orderQuantity uint64 + orderFilled uint64 + orderStatus order.OrderStatus + rate uint64 + sell bool + previousAccelerations []order.CoinID + matches []testMatch + expectRequiredForRemaining uint64 + expectError bool + orderIDIncorrectLength bool + nonActiveOrderID bool + accelerateOrderError bool + nilChangeCoin bool + nilNewChangeCoin bool + }{ + { + name: "ok", + orderQuantity: 3 * dcrBtcLotSize, + orderFilled: dcrBtcLotSize, + previousAccelerations: []order.CoinID{encode.RandomBytes(32)}, + orderStatus: order.OrderStatusExecuted, + rate: dcrBtcRateStep * 10, + expectRequiredForRemaining: 2*tMaxFeeRate*tUTXOAssetB.SwapSize + calc.BaseToQuote(dcrBtcRateStep*10, 2*dcrBtcLotSize), + matches: []testMatch{ + { + side: order.Maker, + status: order.TakerSwapCast, + quantity: dcrBtcLotSize, + rate: dcrBtcRateStep * 10, }, }, - UserMatch: &order.UserMatch{ - MatchID: match1ID, - Address: addr, - Side: order.Maker, - Status: order.TakerSwapCast, + }, + { + name: "ok - unswapped match, buy", + orderQuantity: 8 * dcrBtcLotSize, + orderFilled: 5 * dcrBtcLotSize, + orderStatus: order.OrderStatusExecuted, + previousAccelerations: []order.CoinID{encode.RandomBytes(32), encode.RandomBytes(32)}, + rate: dcrBtcRateStep * 10, + expectRequiredForRemaining: 4*tMaxFeeRate*tUTXOAssetB.SwapSize + calc.BaseToQuote(dcrBtcRateStep*10, 5*dcrBtcLotSize), + matches: []testMatch{ + { + side: order.Maker, + status: order.TakerSwapCast, + quantity: dcrBtcLotSize, + rate: dcrBtcRateStep * 10, + }, + { + side: order.Taker, + status: order.TakerSwapCast, + quantity: 2 * dcrBtcLotSize, + rate: dcrBtcRateStep * 10, + }, + { + side: order.Taker, + status: order.MakerSwapCast, + quantity: 2 * dcrBtcLotSize, + rate: dcrBtcRateStep * 10, + }, }, }, - } - trade.matches[match1ID] = match1 - - match2ID := ordertest.RandomMatchID() - match2 := &matchTracker{ - MetaMatch: db.MetaMatch{ - MetaData: &db.MatchMetaData{ - Proof: db.MatchProof{ - MakerSwap: encode.RandomBytes(32), - TakerSwap: encode.RandomBytes(32), + { + name: "ok - unswapped match, sell", + sell: true, + previousAccelerations: []order.CoinID{encode.RandomBytes(32), encode.RandomBytes(32)}, + orderQuantity: 8 * dcrBtcLotSize, + orderFilled: 5 * dcrBtcLotSize, + orderStatus: order.OrderStatusExecuted, + rate: dcrBtcRateStep * 10, + expectRequiredForRemaining: 4*tMaxFeeRate*tUTXOAssetB.SwapSize + 5*dcrBtcLotSize, + matches: []testMatch{ + { + side: order.Maker, + status: order.TakerSwapCast, + quantity: dcrBtcLotSize, + rate: dcrBtcRateStep * 10, + }, + { + side: order.Taker, + status: order.TakerSwapCast, + quantity: 2 * dcrBtcLotSize, + rate: dcrBtcRateStep * 10, + }, + { + side: order.Taker, + status: order.MakerSwapCast, + quantity: 2 * dcrBtcLotSize, + rate: dcrBtcRateStep * 10, }, }, - UserMatch: &order.UserMatch{ - MatchID: match2ID, - Address: addr, - Side: order.Taker, - Status: order.TakerSwapCast, + }, + { + name: "10 previous accelerations", + sell: true, + previousAccelerations: []order.CoinID{encode.RandomBytes(32), encode.RandomBytes(32), + encode.RandomBytes(32), encode.RandomBytes(32), + encode.RandomBytes(32), encode.RandomBytes(32), + encode.RandomBytes(32), encode.RandomBytes(32), + encode.RandomBytes(32), encode.RandomBytes(32)}, + orderQuantity: 8 * dcrBtcLotSize, + orderFilled: 5 * dcrBtcLotSize, + orderStatus: order.OrderStatusExecuted, + rate: dcrBtcRateStep * 10, + matches: []testMatch{ + { + side: order.Maker, + status: order.TakerSwapCast, + quantity: dcrBtcLotSize, + rate: dcrBtcRateStep * 10, + }, + }, + expectError: true, + }, + { + name: "no matches", + orderQuantity: 3 * dcrBtcLotSize, + orderFilled: dcrBtcLotSize, + orderStatus: order.OrderStatusExecuted, + rate: dcrBtcRateStep * 10, + matches: []testMatch{}, + expectError: true, + }, + { + name: "no swap coins", + orderQuantity: 3 * dcrBtcLotSize, + orderFilled: dcrBtcLotSize, + orderStatus: order.OrderStatusExecuted, + rate: dcrBtcRateStep * 10, + matches: []testMatch{{ + side: order.Taker, + status: order.MakerSwapCast, + quantity: 2 * dcrBtcLotSize, + rate: dcrBtcRateStep * 10, + }}, + expectError: true, + }, + { + name: "incorrect length order id", + orderQuantity: 3 * dcrBtcLotSize, + orderFilled: dcrBtcLotSize, + orderStatus: order.OrderStatusExecuted, + rate: dcrBtcRateStep * 10, + matches: []testMatch{ + { + side: order.Maker, + status: order.TakerSwapCast, + quantity: dcrBtcLotSize, + rate: dcrBtcRateStep * 10, + }, + }, + orderIDIncorrectLength: true, + expectError: true, + }, + { + name: "incorrect length order id", + orderQuantity: 3 * dcrBtcLotSize, + orderFilled: dcrBtcLotSize, + orderStatus: order.OrderStatusExecuted, + rate: dcrBtcRateStep * 10, + matches: []testMatch{ + { + side: order.Maker, + status: order.TakerSwapCast, + quantity: dcrBtcLotSize, + rate: dcrBtcRateStep * 10, + }, + }, + nonActiveOrderID: true, + expectError: true, + }, + { + name: "accelerate order err", + orderQuantity: 3 * dcrBtcLotSize, + orderFilled: dcrBtcLotSize, + orderStatus: order.OrderStatusExecuted, + rate: dcrBtcRateStep * 10, + matches: []testMatch{ + { + side: order.Maker, + status: order.TakerSwapCast, + quantity: dcrBtcLotSize, + rate: dcrBtcRateStep * 10, + }, + }, + accelerateOrderError: true, + expectError: true, + }, + { + name: "nil change coin", + orderQuantity: 3 * dcrBtcLotSize, + orderFilled: dcrBtcLotSize, + orderStatus: order.OrderStatusExecuted, + rate: dcrBtcRateStep * 10, + matches: []testMatch{ + { + side: order.Maker, + status: order.TakerSwapCast, + quantity: dcrBtcLotSize, + rate: dcrBtcRateStep * 10, + }, + }, + nilChangeCoin: true, + expectError: true, + }, + { + name: "nil new change coin", + orderQuantity: 3 * dcrBtcLotSize, + orderFilled: dcrBtcLotSize, + orderStatus: order.OrderStatusExecuted, + rate: dcrBtcRateStep * 10, + expectRequiredForRemaining: 2*tMaxFeeRate*tUTXOAssetB.SwapSize + calc.BaseToQuote(dcrBtcRateStep*10, 2*dcrBtcLotSize), + matches: []testMatch{ + { + side: order.Maker, + status: order.TakerSwapCast, + quantity: dcrBtcLotSize, + rate: dcrBtcRateStep * 10, + }, }, + nilNewChangeCoin: true, }, } - trade.matches[match2ID] = match2 - trade.metaData.ChangeCoin = encode.RandomBytes(32) - originalChangeCoin := trade.metaData.ChangeCoin - trade.metaData.AccelerationCoins = []order.CoinID{encode.RandomBytes(32)} - tBtcWallet.newChangeCoinID = encode.RandomBytes(32) - tBtcWallet.newAccelerationTxID = hex.EncodeToString(encode.RandomBytes(32)) + for _, test := range tests { + tBtcWallet.accelerateOrderErr = nil + lo, dbOrder, preImg, addr := makeLimitOrder(dc, test.sell, test.orderQuantity, test.rate) + dbOrder.MetaData.Status = test.orderStatus // so there is no order_status request for this + oid := lo.ID() + var walletSet *walletSet + if test.sell { + walletSet = sellWalletSet + } else { + walletSet = buyWalletSet + } + trade := newTrackedTrade(dbOrder, preImg, dc, mkt.EpochLen, rig.core.lockTimeTaker, rig.core.lockTimeMaker, + rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails, nil, 0, 0) + dc.trades[trade.ID()] = trade + trade.Trade().AddFill(test.orderFilled) + + trade.metaData.ChangeCoin = encode.RandomBytes(32) + originalChangeCoin := trade.metaData.ChangeCoin + trade.metaData.AccelerationCoins = test.previousAccelerations + newChangeCoinID := dex.Bytes(encode.RandomBytes(32)) + if test.nilNewChangeCoin { + tBtcWallet.newChangeCoinID = nil + } else { + tBtcWallet.newChangeCoinID = &newChangeCoinID + } + tBtcWallet.newAccelerationTxID = hex.EncodeToString(encode.RandomBytes(32)) + trade.matches = make(map[order.MatchID]*matchTracker) + expectedSwapCoins := make([]order.CoinID, 0, len(test.matches)) + for _, testMatch := range test.matches { + matchID := ordertest.RandomMatchID() + match := &matchTracker{ + MetaMatch: db.MetaMatch{ + MetaData: &db.MatchMetaData{ + Proof: db.MatchProof{ + MakerSwap: encode.RandomBytes(32), + TakerSwap: encode.RandomBytes(32), + }, + }, + UserMatch: &order.UserMatch{ + MatchID: matchID, + Address: addr, + Side: testMatch.side, + Status: testMatch.status, + Quantity: testMatch.quantity, + Rate: testMatch.rate, + }, + }, + } + if testMatch.side == order.Maker && testMatch.status >= order.MakerSwapCast { + expectedSwapCoins = append(expectedSwapCoins, match.MetaData.Proof.MakerSwap) + } + if testMatch.side == order.Taker && testMatch.status >= order.TakerSwapCast { + expectedSwapCoins = append(expectedSwapCoins, match.MetaData.Proof.TakerSwap) + } + trade.matches[matchID] = match + } + orderIDBytes := oid.Bytes() + if test.orderIDIncorrectLength { + orderIDBytes = encode.RandomBytes(31) + } + if test.nonActiveOrderID { + orderIDBytes = encode.RandomBytes(32) + } + if test.accelerateOrderError { + tBtcWallet.accelerateOrderErr = errors.New("") + } + if test.nilChangeCoin { + trade.metaData.ChangeCoin = nil + } - txID, err := tCore.AccelerateOrder(tPW, oid.Bytes(), 50) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + checkCommonCallValues := func() { + t.Helper() + swapCoins := tBtcWallet.accelerationParams.swapCoins + if len(swapCoins) != len(expectedSwapCoins) { + t.Fatalf("expected %d swap coins but got %d", len(expectedSwapCoins), len(swapCoins)) + } - if len(tBtcWallet.accelerationParams.swapCoins) != 2 { - t.Fatalf("expected 2 swap coins but got %v", len(tBtcWallet.accelerationParams.swapCoins)) - } - expectedSwapCoins := []order.CoinID{match1.MetaMatch.MetaData.Proof.MakerSwap, match2.MetaMatch.MetaData.Proof.TakerSwap} - swapCoins := tBtcWallet.accelerationParams.swapCoins - if !((bytes.Equal(swapCoins[0], expectedSwapCoins[0]) && bytes.Equal(swapCoins[1], expectedSwapCoins[1])) || - (bytes.Equal(swapCoins[0], expectedSwapCoins[1]) && bytes.Equal(swapCoins[1], expectedSwapCoins[0]))) { - t.Fatalf("swap coins not same as expected") - } + sort.Slice(swapCoins, func(i, j int) bool { return bytes.Compare(swapCoins[i], swapCoins[j]) > 0 }) + sort.Slice(expectedSwapCoins, func(i, j int) bool { return bytes.Compare(expectedSwapCoins[i], expectedSwapCoins[j]) > 0 }) - if !bytes.Equal(tBtcWallet.accelerationParams.changeCoin, originalChangeCoin) { - t.Fatalf("change coin not same as expected %x - %x", tBtcWallet.accelerationParams.changeCoin, trade.metaData.ChangeCoin) - } + for i := range swapCoins { + if !bytes.Equal(swapCoins[i], expectedSwapCoins[i]) { + t.Fatalf("expected swap coins not the same as actual") + } + } - accelerationCoins := tBtcWallet.accelerationParams.accelerationCoins - if len(accelerationCoins) != 1 { - t.Fatalf("expected 1 acceleration tx but got %v", len(accelerationCoins)) - } - if !bytes.Equal(accelerationCoins[0], trade.metaData.AccelerationCoins[0]) { - t.Fatalf("acceleration tx id not same as expected") - } + changeCoin := tBtcWallet.accelerationParams.changeCoin + if !bytes.Equal(changeCoin, originalChangeCoin) { + t.Fatalf("change coin not same as expected %x - %x", changeCoin, trade.metaData.ChangeCoin) + } - if !bytes.Equal(trade.metaData.ChangeCoin, tBtcWallet.newChangeCoinID) { - t.Fatalf("change coin on trade was not updated to return value from AccelerateOrder") - } - if !bytes.Equal(trade.metaData.AccelerationCoins[len(trade.metaData.AccelerationCoins)-1], tBtcWallet.newChangeCoinID) { - t.Fatalf("new acceleration transaction id was not added to the trade") - } - if txID != tBtcWallet.newAccelerationTxID { - t.Fatalf("new acceleration transaction id was not returned from AccelerateOrder") - } + accelerationCoins := tBtcWallet.accelerationParams.accelerationCoins + if len(accelerationCoins) != len(test.previousAccelerations) { + t.Fatalf("expected 1 acceleration tx but got %v", len(accelerationCoins)) + } + for i := range accelerationCoins { + if !bytes.Equal(accelerationCoins[i], test.previousAccelerations[i]) { + t.Fatalf("expected acceleration coin not the same as actual") + } + } + } - var inCoinsList bool - for _, coin := range trade.coins { - if bytes.Equal(coin.ID(), tBtcWallet.newChangeCoinID) { - inCoinsList = true + checkRequiredForRemainingSwaps := func() { + t.Helper() + if tBtcWallet.accelerationParams.requiredForRemainingSwaps != test.expectRequiredForRemaining { + t.Fatalf("expected requiredForRemainingSwaps %d, but got %d", test.expectRequiredForRemaining, + tBtcWallet.accelerationParams.requiredForRemainingSwaps) + } } - } - if !inCoinsList { - t.Fatalf("new change coin must be added to the trade.coins slice") - } - // Ensure error with order id with incorrect length - _, err = tCore.AccelerateOrder(tPW, encode.RandomBytes(31), 50) - if err == nil { - t.Fatalf("expected error but did not get") - } + testAccelerateOrder := func() { + newFeeRate := rand.Uint64() + txID, err := tCore.AccelerateOrder(tPW, orderIDBytes, newFeeRate) + if test.expectError { + if err == nil { + t.Fatalf("expected error, but did not get") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - // Ensure error with non active order id - _, err = tCore.AccelerateOrder(tPW, encode.RandomBytes(order.OrderIDSize), 50) - if err == nil { - t.Fatalf("expected error but did not get") - } + checkCommonCallValues() + checkRequiredForRemainingSwaps() - // Ensure error when wallet method returns an error - tBtcWallet.accelerateOrderErr = errors.New("") - _, err = tCore.AccelerateOrder(tPW, oid.Bytes(), 50) - if err == nil { - t.Fatalf("expected error but did not get") - } + if test.nilNewChangeCoin { + if tBtcWallet.newChangeCoinID != nil { + t.Fatalf("expected coin on order to be nil, but got %x", tBtcWallet.newChangeCoinID) + } + } else { + if !bytes.Equal(trade.metaData.ChangeCoin, *tBtcWallet.newChangeCoinID) { + t.Fatalf("change coin on trade was not updated to return value from AccelerateOrder") + } + if !bytes.Equal(trade.metaData.AccelerationCoins[len(trade.metaData.AccelerationCoins)-1], *tBtcWallet.newChangeCoinID) { + t.Fatalf("new acceleration transaction id was not added to the trade") + } - // Ensure error when change coin is not set - trade.metaData.ChangeCoin = nil - _, err = tCore.AccelerateOrder(tPW, oid.Bytes(), 50) - if err == nil { - t.Fatalf("expected error but did not get") + var inCoinsList bool + for _, coin := range trade.coins { + if bytes.Equal(coin.ID(), *tBtcWallet.newChangeCoinID) { + inCoinsList = true + } + } + if !inCoinsList { + t.Fatalf("new change coin must be added to the trade.coins slice") + } + } + if txID != tBtcWallet.newAccelerationTxID { + t.Fatalf("new acceleration transaction id was not returned from AccelerateOrder") + } + if newFeeRate != tBtcWallet.accelerationParams.newFeeRate { + t.Fatalf("%s: expected new fee rate %d, but got %d", test.name, + newFeeRate, tBtcWallet.accelerationParams.newFeeRate) + } + } + + testPreAccelerate := func() { + rig.ws.queueResponse(msgjson.FeeRateRoute, feeRateSource) + tBtcWallet.preAccelerateSwapRate = rand.Uint64() + tBtcWallet.preAccelerateSuggestedRange = asset.XYRange{ + Start: asset.XYRangePoint{ + Label: "startLabel", + X: rand.Float64(), + Y: rand.Float64(), + }, + End: asset.XYRangePoint{ + Label: "endLabel", + X: rand.Float64(), + Y: rand.Float64(), + }, + XUnit: "x", + YUnit: "y", + } + + preAccelerate, err := tCore.PreAccelerateOrder(orderIDBytes) + if test.expectError { + if err == nil { + t.Fatalf("expected error, but did not get") + } + return + } + if err != nil { + t.Fatalf("%s: unexpected error: %v", test.name, err) + } + + checkCommonCallValues() + checkRequiredForRemainingSwaps() + + if !test.sell && preAccelerate.SuggestedRate != newQuoteFeeRate { + t.Fatalf("%s: expected fee suggestion to be %d, but got %d", + test.name, newQuoteFeeRate, preAccelerate.SuggestedRate) + } + if test.sell && preAccelerate.SuggestedRate != newBaseFeeRate { + t.Fatalf("%s: expected fee suggestion to be %d, but got %d", + test.name, newBaseFeeRate, preAccelerate.SuggestedRate) + } + if preAccelerate.SwapRate != tBtcWallet.preAccelerateSwapRate { + t.Fatalf("%s: expected pre accelerate swap rate %d, but got %d", + test.name, tBtcWallet.preAccelerateSwapRate, preAccelerate.SwapRate) + } + if !reflect.DeepEqual(preAccelerate.SuggestedRange, + tBtcWallet.preAccelerateSuggestedRange) { + t.Fatalf("%s: PreAccelerate suggested range not same as expected", + test.name) + } + } + + testMaxAcceleration := func() { + t.Helper() + tBtcWallet.accelerationEstimate = rand.Uint64() + newFeeRate := rand.Uint64() + estimate, err := tCore.AccelerationEstimate(orderIDBytes, newFeeRate) + if test.expectError { + if err == nil { + t.Fatalf("expected error, but did not get") + } + return + } + if err != nil { + t.Fatalf("%s: unexpected error: %v", test.name, err) + } + + checkCommonCallValues() + checkRequiredForRemainingSwaps() + + if newFeeRate != tBtcWallet.accelerationParams.newFeeRate { + t.Fatalf("%s: expected new fee rate %d, but got %d", test.name, + newFeeRate, tBtcWallet.accelerationParams.newFeeRate) + } + if estimate != tBtcWallet.accelerationEstimate { + t.Fatalf("%s: expected acceleration estimate %d, but got %d", + test.name, tBtcWallet.accelerationEstimate, estimate) + } + } + + testPreAccelerate() + testMaxAcceleration() + testAccelerateOrder() } } diff --git a/client/core/trade.go b/client/core/trade.go index 41f23896df..bee8e1ca6d 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -2783,6 +2783,93 @@ func (t *trackedTrade) returnCoins() { } } +// requiredForRemainingSwaps determines the amount of the from asset that is +// still needed in order initate the remaining swaps in the order. +func (t *trackedTrade) requiredForRemainingSwaps() (uint64, error) { + mkt := t.dc.marketConfig(t.mktID) + if mkt == nil { + return 0, fmt.Errorf("could not find market: %v", t.mktID) + } + lotSize := mkt.LotSize + swapSize := t.wallets.fromAsset.SwapSize + + var requiredForRemainingSwaps uint64 + + if t.metaData.Status <= order.OrderStatusExecuted { + if !t.Trade().Sell { + requiredForRemainingSwaps += calc.BaseToQuote(t.rate(), t.Trade().Remaining()) + } else { + requiredForRemainingSwaps += t.Trade().Remaining() + } + lotsRemaining := t.Trade().Remaining() / lotSize + requiredForRemainingSwaps += lotsRemaining * swapSize * t.metaData.MaxFeeRate + } + + for _, match := range t.matches { + if (match.Side == order.Maker && match.Status < order.MakerSwapCast) || + (match.Side == order.Taker && match.Status < order.TakerSwapCast) { + if !t.Trade().Sell { + requiredForRemainingSwaps += calc.BaseToQuote(match.Rate, match.Quantity) + } else { + requiredForRemainingSwaps += match.Quantity + } + requiredForRemainingSwaps += swapSize * t.metaData.MaxFeeRate + } + } + + return requiredForRemainingSwaps, nil +} + +// orderAccelerationParameters returns the parameters needed to accelerate the +// swap transactions in this trade. +// MUST be called with the trackedTrade mutex held. +func (t *trackedTrade) orderAccelerationParameters() (swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps uint64, err error) { + makeError := func(err error) ([]dex.Bytes, []dex.Bytes, dex.Bytes, uint64, error) { + return nil, nil, nil, 0, err + } + + if t.metaData.ChangeCoin == nil { + return makeError(fmt.Errorf("order does not have change which can be accelerated")) + } + + if len(t.metaData.AccelerationCoins) >= 10 { + return makeError(fmt.Errorf("order has already been accelerated too many times")) + } + + requiredForRemainingSwaps, err = t.requiredForRemainingSwaps() + if err != nil { + return makeError(err) + } + + swapCoins = make([]dex.Bytes, 0, len(t.matches)) + for _, match := range t.matches { + if match.Status < order.MakerSwapCast { + continue + } + var swapCoinID order.CoinID + if match.Side == order.Maker { + swapCoinID = match.MetaData.Proof.MakerSwap + } else { + if match.Status < order.TakerSwapCast { + continue + } + swapCoinID = match.MetaData.Proof.TakerSwap + } + swapCoins = append(swapCoins, dex.Bytes(swapCoinID)) + } + + if len(swapCoins) == 0 { + return makeError(fmt.Errorf("cannot accelerate an order without any swaps")) + } + + accelerationCoins = make([]dex.Bytes, 0, len(t.metaData.AccelerationCoins)) + for _, coin := range t.metaData.AccelerationCoins { + accelerationCoins = append(accelerationCoins, dex.Bytes(coin)) + } + + return swapCoins, accelerationCoins, dex.Bytes(t.metaData.ChangeCoin), requiredForRemainingSwaps, nil +} + // mapifyCoins converts the slice of coins to a map keyed by hex coin ID. func mapifyCoins(coins asset.Coins) map[string]asset.Coin { coinMap := make(map[string]asset.Coin, len(coins)) diff --git a/client/webserver/api.go b/client/webserver/api.go index 63f3f21d20..3a3c6051f7 100644 --- a/client/webserver/api.go +++ b/client/webserver/api.go @@ -606,8 +606,7 @@ func (s *WebServer) apiOrders(w http.ResponseWriter, r *http.Request) { }, s.indent) } -// apiAccelerateOrder uses the Child-Pays-For-Parent technique to speen up an -// order. +// apiAccelerateOrder speeds up the mining of transactions in an order. func (s *WebServer) apiAccelerateOrder(w http.ResponseWriter, r *http.Request) { form := struct { Pass encode.PassBytes `json:"pw"` diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index 254acfd9c9..7fceec2c8d 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -216,11 +216,11 @@ var EnUS = map[string]string{ "max_fee_conditions": "This is the most you would ever pay in fees on your swap. Fees are normally assessed at a fraction of this rate. The maximum is not subject to changes once your order is placed.", "wallet_logs": "Wallet Logs", "accelerate_order": "Accelerate Order", - "acceleration_text": "In case your swap transactions are stuck, you can accelerate them using the Child Pays For Parent technique. When you submit this form, you will create a transaction that sends the change recieved from initiating swaps to yourself with a higher fee. With the new transaction, the average fee rate of all unmined swap transactions, and the new acceleration transaction, will be the rate that you select below.", - "avg_swap_tx_rate": "Average swap tx fee rate", + "acceleration_text": "If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block.", + "effective_swap_tx_rate": "Effective swap tx fee rate", "current_fee": "Current suggested fee rate", "accelerate_success": `Successfully submitted transaction: `, "accelerate": "Accelerate", "acceleration_transactions": "Acceleration Transactions", - "acceleration_cost_msg": `Increasing the fee rate to will cost `, + "acceleration_cost_msg": `Increasing the effective fee rate to will cost `, } diff --git a/client/webserver/site/src/html/order.tmpl b/client/webserver/site/src/html/order.tmpl index 3eaa54c268..445c5c8b50 100644 --- a/client/webserver/site/src/html/order.tmpl +++ b/client/webserver/site/src/html/order.tmpl @@ -232,7 +232,7 @@ [[[acceleration_text]]]
- [[[avg_swap_tx_rate]]]: + [[[effective_swap_tx_rate]]]:
[[[current_fee]]]: diff --git a/client/webserver/site/src/localized_html/en-US/order.tmpl b/client/webserver/site/src/localized_html/en-US/order.tmpl index 4667df4f01..7873eb9505 100644 --- a/client/webserver/site/src/localized_html/en-US/order.tmpl +++ b/client/webserver/site/src/localized_html/en-US/order.tmpl @@ -229,10 +229,10 @@ Accelerate Order
- In case your swap transactions are stuck, you can accelerate them using the Child Pays For Parent technique. When you submit this form, you will create a transaction that sends the change recieved from initiating swaps to yourself with a higher fee. With the new transaction, the average fee rate of all unmined swap transactions, and the new acceleration transaction, will be the rate that you select below. + If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block.
- Average swap tx fee rate: + Effective swap tx fee rate:
Current suggested fee rate: @@ -240,7 +240,7 @@
- Increasing the fee rate to will cost + Increasing the effective fee rate to will cost

diff --git a/client/webserver/site/src/localized_html/pl-PL/order.tmpl b/client/webserver/site/src/localized_html/pl-PL/order.tmpl index 58110043e2..460821fea6 100644 --- a/client/webserver/site/src/localized_html/pl-PL/order.tmpl +++ b/client/webserver/site/src/localized_html/pl-PL/order.tmpl @@ -229,10 +229,10 @@ Accelerate Order
- In case your swap transactions are stuck, you can accelerate them using the Child Pays For Parent technique. When you submit this form, you will create a transaction that sends the change recieved from initiating swaps to yourself with a higher fee. With the new transaction, the average fee rate of all unmined swap transactions, and the new acceleration transaction, will be the rate that you select below. + If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block.
- Average swap tx fee rate: + Effective swap tx fee rate:
Current suggested fee rate: @@ -240,7 +240,7 @@
- Increasing the fee rate to will cost + Increasing the effective fee rate to will cost

diff --git a/client/webserver/site/src/localized_html/pt-BR/order.tmpl b/client/webserver/site/src/localized_html/pt-BR/order.tmpl index 55a1e3fe8a..37c4458067 100644 --- a/client/webserver/site/src/localized_html/pt-BR/order.tmpl +++ b/client/webserver/site/src/localized_html/pt-BR/order.tmpl @@ -229,10 +229,10 @@ Accelerate Order
- In case your swap transactions are stuck, you can accelerate them using the Child Pays For Parent technique. When you submit this form, you will create a transaction that sends the change recieved from initiating swaps to yourself with a higher fee. With the new transaction, the average fee rate of all unmined swap transactions, and the new acceleration transaction, will be the rate that you select below. + If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block.
- Average swap tx fee rate: + Effective swap tx fee rate:
Current suggested fee rate: @@ -240,7 +240,7 @@
- Increasing the fee rate to will cost + Increasing the effective fee rate to will cost

diff --git a/client/webserver/site/src/localized_html/zh-CN/order.tmpl b/client/webserver/site/src/localized_html/zh-CN/order.tmpl index 0953bc876d..f55509a7b7 100644 --- a/client/webserver/site/src/localized_html/zh-CN/order.tmpl +++ b/client/webserver/site/src/localized_html/zh-CN/order.tmpl @@ -229,10 +229,10 @@ Accelerate Order
- In case your swap transactions are stuck, you can accelerate them using the Child Pays For Parent technique. When you submit this form, you will create a transaction that sends the change recieved from initiating swaps to yourself with a higher fee. With the new transaction, the average fee rate of all unmined swap transactions, and the new acceleration transaction, will be the rate that you select below. + If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block.
- Average swap tx fee rate: + Effective swap tx fee rate:
Current suggested fee rate: @@ -240,7 +240,7 @@
- Increasing the fee rate to will cost + Increasing the effective fee rate to will cost

diff --git a/dex/networks/btc/script.go b/dex/networks/btc/script.go index 3807831eab..ef23a3919e 100644 --- a/dex/networks/btc/script.go +++ b/dex/networks/btc/script.go @@ -170,14 +170,19 @@ const ( // NOTE: witness data is not script. RedeemP2WPKHInputWitnessWeight = 1 + 1 + DERSigLength + 1 + 33 // 109 - // RedeemP2PKHInputTotalSize is the worst case size of a transaction + // RedeemP2WPKHInputTotalSize is the worst case size of a transaction // input redeeming a P2WPKH output and the corresponding witness data. // It is calculated as: // // 41 vbytes base tx input - // 109wu witness + 2wu segwit marker and flag = 28 vbytes + // 109wu witness = 28 vbytes // total = 69 vbytes - RedeemP2PWKHInputTotalSize = RedeemP2WPKHInputSize + ((RedeemP2WPKHInputWitnessWeight + 2 + 3) / 4) + RedeemP2WPKHInputTotalSize = RedeemP2WPKHInputSize + + (RedeemP2WPKHInputWitnessWeight+(witnessWeight-1))/witnessWeight + + // SigwitMarkerAndFlagWeight is the 2 bytes of overhead witness data + // added to every segwit transaction. + SegwitMarkerAndFlagWeight = 2 // RedeemP2WSHInputWitnessWeight depends on the number of redeem scrpit and // number of signatures. @@ -227,7 +232,8 @@ const ( // 41 vbytes base tx input // 109wu witness + 2wu segwit marker and flag = 28 vbytes // total = 153 vbytes - InitTxSizeSegwit = InitTxSizeBaseSegwit + RedeemP2PWKHInputTotalSize + InitTxSizeSegwit = InitTxSizeBaseSegwit + RedeemP2WPKHInputSize + + (SegwitMarkerAndFlagWeight+RedeemP2WPKHInputWitnessWeight+(witnessWeight-1))/witnessWeight witnessWeight = blockchain.WitnessScaleFactor ) diff --git a/dex/order/order.go b/dex/order/order.go index 3c1b742f83..7bd7426f03 100644 --- a/dex/order/order.go +++ b/dex/order/order.go @@ -45,7 +45,7 @@ func IDFromHex(sid string) (OrderID, error) { return oid, nil } -// IDFromHex converts a byte slice to an OrderID. +// IDFromBytes converts a byte slice to an OrderID. func IDFromBytes(b []byte) (OrderID, error) { if len(b) != OrderIDSize { return OrderID{}, fmt.Errorf("invalid order ID. wanted length %d but got %d", OrderIDSize, len(b)) From 3c3c8b2ec95b1a6346a8d793dc4aaf786688cad9 Mon Sep 17 00:00:00 2001 From: martonp Date: Tue, 26 Apr 2022 10:58:52 +0700 Subject: [PATCH 07/13] Selectively enable acceleration for btc clones. --- client/asset/btc/btc.go | 24 +++++++++++++++++++++++- client/asset/interface.go | 7 ++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index a1aa1048ae..3bbcaa778c 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -270,6 +270,8 @@ type BTCCloneCFG struct { // output value) that doesn't depend on the serialized size of the output. // If ConstantDustLimit is zero, dexbtc.IsDust is used. ConstantDustLimit uint64 + // SupportsCPFP is true if the wallet supports child pays for parent. + SupportsCPFP bool } // outPoint is the hash and output index of a transaction output. @@ -591,6 +593,7 @@ type baseWallet struct { decodeAddr dexbtc.AddressDecoder stringAddr dexbtc.AddressStringer net dex.Network + supportsCPFP bool tipMtx sync.RWMutex currentTip *block @@ -615,7 +618,6 @@ type ExchangeWalletFullNode struct { } // Check that wallets satisfy their supported interfaces. - var _ asset.Wallet = (*baseWallet)(nil) var _ asset.Accelerator = (*baseWallet)(nil) var _ asset.Rescanner = (*ExchangeWalletSPV)(nil) @@ -718,6 +720,7 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, net dex.Network) (ass DefaultFallbackFee: defaultFee, DefaultFeeRateLimit: defaultFeeRateLimit, Segwit: true, + SupportsCPFP: true, } switch cfg.Type { @@ -862,6 +865,7 @@ func newUnconnectedWallet(cfg *BTCCloneCFG, walletCfg *WalletConfig) (*baseWalle stringAddr: addrStringer, walletInfo: cfg.WalletInfo, net: cfg.Network, + supportsCPFP: cfg.SupportsCPFP, } if w.estimateFee == nil { @@ -2046,6 +2050,12 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B return tx, output, txFee + additionalFeesRequired, nil } +// CanAccelerate returns whether or not the wallet supports acceleration. +// Some of the BTC clones do not support it. +func (btc *baseWallet) CanAccelerate() bool { + return btc.supportsCPFP +} + // AccelerateOrder uses the Child-Pays-For-Parent technique to accelerate a // chain of swap transactions and previous accelerations. It broadcasts a new // transaction with a fee high enough so that the average fee of all the @@ -2056,6 +2066,10 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B // // The returned change coin may be nil, and should be checked before use. func (btc *baseWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { + if !btc.supportsCPFP { + return nil, "", fmt.Errorf("this wallet does not support acceleration") + } + btc.fundingMtx.Lock() defer btc.fundingMtx.Unlock() @@ -2113,6 +2127,10 @@ func (btc *baseWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, // the amount of funds that will need to be spent in order to increase the // average fee rate to the desired amount. func (btc *baseWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) { + if !btc.supportsCPFP { + return 0, fmt.Errorf("this wallet does not support acceleration") + } + btc.fundingMtx.RLock() defer btc.fundingMtx.RUnlock() _, _, fee, err := btc.signedAccelerationTx(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) @@ -2179,6 +2197,10 @@ func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, c return 0, asset.XYRange{}, err } + if !btc.supportsCPFP { + return makeError(fmt.Errorf("this wallet does not support acceleration")) + } + changeTxHash, changeVout, err := decodeCoinID(changeCoin) if err != nil { return makeError(err) diff --git a/client/asset/interface.go b/client/asset/interface.go index a15a22bf01..277c554348 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -66,7 +66,7 @@ func DetermineWalletTraits(w Wallet) (t WalletTrait) { if _, is := w.(FeeRater); is { t |= WalletTraitFeeRater } - if _, is := w.(Accelerator); is { + if a, is := w.(Accelerator); is && a.CanAccelerate() { t |= WalletTraitAccelerator } return t @@ -394,6 +394,11 @@ type Accelerator interface { // range that the fee rate should be increased to in order to expedite // mining. PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange XYRange, err error) + // CanAccelerate returns whether or not the wallet can accelerate. Ideally + // we would know this just by whether or not the wallet implements this + // interface, but since all bitcoin clones use the same implementation + // and some of them do not support acceleration, this function is needed. + CanAccelerate() bool } // TokenMaster is implemented by assets which support degenerate tokens. From ab027ff80a89556946cfeb96ad57e0aed1d4b702 Mon Sep 17 00:00:00 2001 From: martonp Date: Wed, 27 Apr 2022 12:39:07 +0700 Subject: [PATCH 08/13] Confirmation popup if acceleration too soon. --- client/asset/btc/btc.go | 65 +++++++--------- client/asset/btc/btc_test.go | 75 +++++++++++-------- client/asset/interface.go | 23 +++++- client/core/core.go | 9 ++- client/core/core_test.go | 11 ++- client/core/types.go | 7 +- client/core/wallet.go | 4 +- client/webserver/locales/en-us.go | 3 + client/webserver/site/src/html/order.tmpl | 53 ++++++++----- client/webserver/site/src/js/order.ts | 38 +++++++++- .../site/src/localized_html/en-US/order.tmpl | 53 ++++++++----- .../site/src/localized_html/pl-PL/order.tmpl | 53 ++++++++----- .../site/src/localized_html/pt-BR/order.tmpl | 53 ++++++++----- .../site/src/localized_html/zh-CN/order.tmpl | 53 ++++++++----- 14 files changed, 314 insertions(+), 186 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 3bbcaa778c..7b30fea243 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -1474,6 +1474,7 @@ func (btc *baseWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, er // FundOrder selects coins for use in an order. The coins will be locked, and // will not be returned in subsequent calls to FundOrder or calculated in calls // to Available, unless they are unlocked with ReturnCoins. +// The returned []dex.Bytes contains the redeem scripts for the selected coins. // Equal number of coins and redeemed scripts must be returned. A nil or empty // dex.Bytes should be appended to the redeem scripts collection for coins with // no redeem script. @@ -1971,17 +1972,6 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B return makeError(fmt.Errorf("no additional fees are required to move the fee rate to %v", newFeeRate)) } - if btc.net != dex.Simnet { - tooEarly, minAccelerationTime, err := tooEarlyToAccelerate(txs, accelerationCoins) - if err != nil { - return makeError(err) - } - if tooEarly { - minLocalTime := time.Unix(int64(minAccelerationTime), 0).Local() - return makeError(fmt.Errorf("cannot accelerate until %v", minLocalTime)) - } - } - txSize := uint64(dexbtc.MinimumTxOverhead) if btc.segwit { txSize += dexbtc.RedeemP2WPKHInputTotalSize @@ -2141,16 +2131,17 @@ func (btc *baseWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.B return fee, nil } -// tooEarlyChecks returns true if minTimeBeforeAcceleration has not passed +// tooEarlyToAccelerate returns true if minTimeBeforeAcceleration has not passed // since either the earliest unconfirmed transaction in the chain, or the -// latest acceleration transaction. It also returns the minimum time when -// the user can do an acceleration. -func tooEarlyToAccelerate(txs []*GetTransactionResult, accelerationCoins []dex.Bytes) (bool, uint64, error) { +// latest acceleration transaction. It also returns the time that passed since +// either of these events, and whether or not the event was an acceleration +// transaction. +func tooEarlyToAccelerate(txs []*GetTransactionResult, accelerationCoins []dex.Bytes) (bool, bool, uint64, error) { accelerationTxs := make(map[string]bool, len(accelerationCoins)) for _, accelerationCoin := range accelerationCoins { txHash, _, err := decodeCoinID(accelerationCoin) if err != nil { - return false, 0, err + return false, false, 0, err } accelerationTxs[txHash.String()] = true } @@ -2169,19 +2160,20 @@ func tooEarlyToAccelerate(txs []*GetTransactionResult, accelerationCoins []dex.B } } if latestAcceleration == 0 && earliestUnconfirmed == 0 { - return false, 0, fmt.Errorf("no need to accelerate because all tx are confirmed") + return false, false, 0, fmt.Errorf("no need to accelerate because all tx are confirmed") } - var timeToCompare uint64 + var actionTime uint64 + var wasAccelerated bool if latestAcceleration != 0 { - timeToCompare = latestAcceleration + wasAccelerated = true + actionTime = latestAcceleration } else { - timeToCompare = earliestUnconfirmed + actionTime = earliestUnconfirmed } currentTime := uint64(time.Now().Unix()) - minAccelerationTime := timeToCompare + minTimeBeforeAcceleration - return minAccelerationTime > currentTime, minAccelerationTime, nil + return actionTime+minTimeBeforeAcceleration > currentTime, wasAccelerated, currentTime - actionTime, nil } // PreAccelerate returns the current average fee rate of the unmined swap @@ -2192,9 +2184,9 @@ func tooEarlyToAccelerate(txs []*GetTransactionResult, accelerationCoins []dex.B // the user a good amount of flexibility in determining the post acceleration // effective fee rate, but still not allowing them to pick something // outrageously high. -func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentEffectiveRate uint64, suggestedRange asset.XYRange, err error) { - makeError := func(err error) (uint64, asset.XYRange, error) { - return 0, asset.XYRange{}, err +func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { + makeError := func(err error) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { + return 0, asset.XYRange{}, nil, err } if !btc.supportsCPFP { @@ -2234,14 +2226,15 @@ func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, c return makeError(fmt.Errorf("all transactions are already confirmed, no need to accelerate")) } - if btc.net != dex.Simnet { - tooEarly, minAccelerationTime, err := tooEarlyToAccelerate(txs, accelerationCoins) - if err != nil { - return makeError(err) - } - if tooEarly { - minLocalTime := time.Unix(int64(minAccelerationTime), 0).Local() - return makeError(fmt.Errorf("cannot accelerate until %v", minLocalTime)) + var earlyAcceleration *asset.EarlyAcceleration + tooEarly, isAcceleration, timePast, err := tooEarlyToAccelerate(txs, accelerationCoins) + if err != nil { + return makeError(err) + } + if tooEarly { + earlyAcceleration = &asset.EarlyAcceleration{ + TimePast: timePast, + WasAcclerated: isAcceleration, } } @@ -2290,7 +2283,7 @@ func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, c } maxRate := (changeOutput.value + feesAlreadyPaid + utxosVal - requiredForRemainingSwaps) / (newChangeTxSize + swapTxsSize) - currentEffectiveRate = feesAlreadyPaid / swapTxsSize + currentEffectiveRate := feesAlreadyPaid / swapTxsSize if maxRate <= currentEffectiveRate { return makeError(fmt.Errorf("cannot accelerate, max rate %v <= current rate %v", maxRate, currentEffectiveRate)) @@ -2313,7 +2306,7 @@ func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, c if maxRate < maxSuggestion { maxSuggestion = maxRate } - suggestedRange = asset.XYRange{ + suggestedRange := asset.XYRange{ Start: asset.XYRangePoint{ Label: "Min", X: float64(currentEffectiveRate+1) / float64(currentEffectiveRate), @@ -2328,7 +2321,7 @@ func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, c YUnit: btc.walletInfo.UnitInfo.AtomicUnit + "/" + btc.sizeUnit(), } - return currentEffectiveRate, suggestedRange, nil + return currentEffectiveRate, suggestedRange, earlyAcceleration, nil } // lookupOutput looks up the value of a transaction output and creates an diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index 62237e2e1d..218fc14273 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -626,6 +626,7 @@ func tNewWallet(segwit bool, walletType string) (*ExchangeWalletFullNode, *testD DefaultFallbackFee: defaultFee, DefaultFeeRateLimit: defaultFeeRateLimit, Segwit: segwit, + SupportsCPFP: true, } var wallet *ExchangeWalletFullNode @@ -3320,7 +3321,6 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { ScriptPubKey: scriptPubKey, Spendable: true, Solvable: true, - Safe: true, }) var prevChainHash chainhash.Hash @@ -3444,6 +3444,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { // needed to test PreAccelerate suggestedFeeRate uint64 expectPreAccelerateErr bool + expectTooEarly bool }{ { name: "change not in utxo set", @@ -3644,16 +3645,14 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { }, }, { - name: "tx time within limit", - txTimeWithinLimit: true, - changeAmount: int64(expectedFees), - fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, - confs: confs, - newFeeRate: 50, - suggestedFeeRate: 30, - expectAccelerationEstimateErr: true, - expectAccelerateOrderErr: true, - expectPreAccelerateErr: true, + name: "tx time within limit", + txTimeWithinLimit: true, + expectTooEarly: true, + changeAmount: int64(expectedFees), + fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, + confs: confs, + newFeeRate: 50, + suggestedFeeRate: 30, }, } @@ -3783,7 +3782,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { testAccelerationEstimate() testPreAccelerate := func() { - currentRate, suggestedRange, err := wallet.PreAccelerate(swapCoins, accelerations, changeCoin, test.requiredForRemainingSwaps, test.suggestedFeeRate) + currentRate, suggestedRange, earlyAcceleration, err := wallet.PreAccelerate(swapCoins, accelerations, changeCoin, test.requiredForRemainingSwaps, test.suggestedFeeRate) if test.expectPreAccelerateErr { if err == nil { t.Fatalf("%s: expected PreAccelerate error but did not get", test.name) @@ -3794,6 +3793,10 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { t.Fatalf("%s: unexpected error: %v", test.name, err) } + if test.expectTooEarly != (earlyAcceleration != nil) { + t.Fatalf("%s: expected early acceleration %v, but got %v", test.name, test.expectTooEarly, earlyAcceleration) + } + var totalSize, totalFee uint64 for i, tx := range txs { if test.confs[i] == 0 { @@ -3855,13 +3858,14 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { func TestTooEarlyToAccelerate(t *testing.T) { tests := []struct { - name string - confirmations []uint64 - isAcceleration []bool - secondsBeforeNow []uint64 - expectTooEarly bool - expectMinTimeToAccelerate int64 // seconds from now +-1 - expectError bool + name string + confirmations []uint64 + isAcceleration []bool + secondsBeforeNow []uint64 + expectTooEarly bool + expectTimePast uint64 + expectWasAcceleration bool + expectError bool }{ { name: "all confirmed", @@ -3885,8 +3889,8 @@ func TestTooEarlyToAccelerate(t *testing.T) { minTimeBeforeAcceleration + 300, minTimeBeforeAcceleration + 800, }, - expectTooEarly: false, - expectMinTimeToAccelerate: -500, + expectTooEarly: false, + expectTimePast: minTimeBeforeAcceleration + 500, }, { name: "acceleration after unconfirmed, not too early", @@ -3898,8 +3902,8 @@ func TestTooEarlyToAccelerate(t *testing.T) { minTimeBeforeAcceleration + 800, minTimeBeforeAcceleration + 500, }, - expectTooEarly: false, - expectMinTimeToAccelerate: -300, + expectTooEarly: false, + expectTimePast: minTimeBeforeAcceleration + 300, }, { name: "no accelerations, too early", @@ -3911,8 +3915,9 @@ func TestTooEarlyToAccelerate(t *testing.T) { minTimeBeforeAcceleration - 300, minTimeBeforeAcceleration - 500, }, - expectTooEarly: true, - expectMinTimeToAccelerate: 300, + expectTooEarly: true, + expectWasAcceleration: false, + expectTimePast: minTimeBeforeAcceleration - 300, }, { name: "acceleration after unconfirmed, too early", @@ -3924,8 +3929,9 @@ func TestTooEarlyToAccelerate(t *testing.T) { minTimeBeforeAcceleration + 500, minTimeBeforeAcceleration - 300, }, - expectTooEarly: true, - expectMinTimeToAccelerate: 300, + expectTooEarly: true, + expectWasAcceleration: true, + expectTimePast: minTimeBeforeAcceleration - 300, }, } @@ -3945,7 +3951,7 @@ func TestTooEarlyToAccelerate(t *testing.T) { Time: uint64(now) - test.secondsBeforeNow[i], }) } - tooEarly, minTimeToAccelerate, err := tooEarlyToAccelerate(sortedTxChain, accelerationCoins) + tooEarly, wasAcceleration, actionTime, err := tooEarlyToAccelerate(sortedTxChain, accelerationCoins) if test.expectError { if err == nil { t.Fatalf("%s: expected error but did not get", test.name) @@ -3960,10 +3966,15 @@ func TestTooEarlyToAccelerate(t *testing.T) { t.Fatalf("%s: too early expected: %v, got %v", test.name, test.expectTooEarly, tooEarly) } - if minTimeToAccelerate > uint64(now+test.expectMinTimeToAccelerate+1) || - minTimeToAccelerate < uint64(now+test.expectMinTimeToAccelerate-1) { - t.Fatalf("%s: min time to accelerate expected: %v, got %v", - test.name, test.expectMinTimeToAccelerate, minTimeToAccelerate) + if actionTime > test.expectTimePast+1 || + actionTime < test.expectTimePast-1 { + t.Fatalf("%s: action time expected: %v, got %v", + test.name, test.expectTimePast, actionTime) + } + + // If it is not too early, it doesn't matter what the wasAcceleration return value is + if tooEarly && wasAcceleration != test.expectWasAcceleration { + t.Fatalf("%s: expect was acceleration %v, but got %v", test.name, test.expectWasAcceleration, wasAcceleration) } } } diff --git a/client/asset/interface.go b/client/asset/interface.go index 277c554348..ce2cb44531 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -370,6 +370,21 @@ type FeeRater interface { FeeRate() uint64 } +// EarlyAcceleration is returned from the PreAccelerate function to inform the +// user that either their last acceleration or oldest swap transaction happened +// very recently, and that they should double check that they really want to do +// an acceleration. +type EarlyAcceleration struct { + // TimePast is the amount of seconds that has past since either the previous + // acceleration, or the oldest unmined swap transaction was submitted to + // the blockchain. + TimePast uint64 `json:"timePast"` + // WasAccelerated is true if the action that took place TimePast seconds + // ago was an acceleration. If false, the oldest unmined swap transaction + // in the order was submitted TimePast seconds ago. + WasAcclerated bool `json:"wasAccelerated"` +} + // Accelerator is implemented by wallets which support acceleration of the // mining of swap transactions. type Accelerator interface { @@ -390,10 +405,12 @@ type Accelerator interface { // average fee rate to the desired amount. AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) // PreAccelerate returns the current average fee rate of the unmined swap - // initiation and acceleration transactions, and also returns a suggested + // initiation and acceleration transactions, a suggested // range that the fee rate should be increased to in order to expedite - // mining. - PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange XYRange, err error) + // mining, and also optionally an EarlyAcceleration notification if + // the user's previous acceleration on this order or the earliest + // unmined transaction in this order happened very recently. + PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, XYRange, *EarlyAcceleration, error) // CanAccelerate returns whether or not the wallet can accelerate. Ideally // we would know this just by whether or not the wallet implements this // interface, but since all bitcoin clones use the same implementation diff --git a/client/core/core.go b/client/core/core.go index 903782f03b..727d1b53cb 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -7220,16 +7220,17 @@ func (c *Core) PreAccelerateOrder(oidB dex.Bytes) (*PreAccelerate, error) { return nil, err } - currentRate, suggestedRange, err := + currentRate, suggestedRange, earlyAcceleration, err := tracker.wallets.fromWallet.preAccelerate(swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, feeSuggestion) if err != nil { return nil, err } return &PreAccelerate{ - SwapRate: currentRate, - SuggestedRate: feeSuggestion, - SuggestedRange: suggestedRange, + SwapRate: currentRate, + SuggestedRate: feeSuggestion, + SuggestedRange: suggestedRange, + EarlyAcceleration: earlyAcceleration, }, nil } diff --git a/client/core/core_test.go b/client/core/core_test.go index 80f50dd259..a97adbf68e 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -678,6 +678,7 @@ func newTWallet(assetID uint32) (*xcWallet, *TXCWallet) { synced: true, syncProgress: 1, pw: tPW, + traits: asset.DetermineWalletTraits(w), } return xcWallet, w @@ -903,9 +904,9 @@ func (w *TXCWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, ch return nil, w.newAccelerationTxID, nil } -func (w *TXCWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange asset.XYRange, err error) { +func (w *TXCWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { if w.accelerateOrderErr != nil { - return 0, asset.XYRange{}, w.accelerateOrderErr + return 0, asset.XYRange{}, nil, w.accelerateOrderErr } w.accelerationParams = &struct { @@ -923,7 +924,7 @@ func (w *TXCWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, chan feeSuggestion: feeSuggestion, } - return w.preAccelerateSwapRate, w.preAccelerateSuggestedRange, nil + return w.preAccelerateSwapRate, w.preAccelerateSuggestedRange, nil, nil } func (w *TXCWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) { @@ -949,6 +950,10 @@ func (w *TXCWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Byte return w.accelerationEstimate, nil } +func (w *TXCWallet) CanAccelerate() bool { + return true +} + type TAccountLocker struct { *TXCWallet reserveNRedemptions uint64 diff --git a/client/core/types.go b/client/core/types.go index 9f5f3829a4..a8d6c6c3da 100644 --- a/client/core/types.go +++ b/client/core/types.go @@ -914,7 +914,8 @@ type OrderEstimate struct { // PreAccelerate gives information that the user can use to decide on // how much to accelerate stuck swap transactions in an order. type PreAccelerate struct { - SwapRate uint64 `json:"swapRate"` - SuggestedRate uint64 `json:"suggestedRate"` - SuggestedRange asset.XYRange `json:"suggestedRange"` + SwapRate uint64 `json:"swapRate"` + SuggestedRate uint64 `json:"suggestedRate"` + SuggestedRange asset.XYRange `json:"suggestedRange"` + EarlyAcceleration *asset.EarlyAcceleration `json:"earlyAcceleration,omitempty"` } diff --git a/client/core/wallet.go b/client/core/wallet.go index 16372bf81d..c709b4892c 100644 --- a/client/core/wallet.go +++ b/client/core/wallet.go @@ -325,10 +325,10 @@ func (w *xcWallet) accelerationEstimate(swapCoins, accelerationCoins []dex.Bytes // preAccelerate gives the user information about accelerating an order if the // wallet is an Accelerator. -func (w *xcWallet) preAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (currentRate uint64, suggestedRange asset.XYRange, err error) { +func (w *xcWallet) preAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { accelerator, ok := w.Wallet.(asset.Accelerator) if !ok { - return 0, asset.XYRange{}, errors.New("wallet does not support acceleration") + return 0, asset.XYRange{}, nil, errors.New("wallet does not support acceleration") } return accelerator.PreAccelerate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion) diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index 7fceec2c8d..5250aae595 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -223,4 +223,7 @@ var EnUS = map[string]string{ "accelerate": "Accelerate", "acceleration_transactions": "Acceleration Transactions", "acceleration_cost_msg": `Increasing the effective fee rate to will cost `, + "recent_acceleration_msg": `Your latest acceleration was only minutes ago! Are you sure you want to accelerate?`, + "recent_swap_msg": `Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?`, + "early_acceleration_help_msg": `It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions.`, } diff --git a/client/webserver/site/src/html/order.tmpl b/client/webserver/site/src/html/order.tmpl index 445c5c8b50..d721e3ff8e 100644 --- a/client/webserver/site/src/html/order.tmpl +++ b/client/webserver/site/src/html/order.tmpl @@ -229,27 +229,40 @@ [[[accelerate_order]]]
- [[[acceleration_text]]] -
-
- [[[effective_swap_tx_rate]]]: -
-
- [[[current_fee]]]: -
-
-
-
- [[[acceleration_cost_msg]]] -
-
-
-
- - +
+ [[[acceleration_text]]] +
+
+ [[[effective_swap_tx_rate]]]: +
+
+ [[[current_fee]]]: +
+
-
- +
+ [[[acceleration_cost_msg]]] +
+
+
+
+ + +
+
+ +
+
+
+
+
[[[recent_acceleration_msg]]]
+
[[[recent_swap_msg]]]
+
+ [[[early_acceleration_help_msg]]] +
+
+ +
diff --git a/client/webserver/site/src/js/order.ts b/client/webserver/site/src/js/order.ts index 3c1380eda8..e65960e134 100644 --- a/client/webserver/site/src/js/order.ts +++ b/client/webserver/site/src/js/order.ts @@ -23,10 +23,16 @@ const animationLength = 500 let net: number +interface EarlyAcceleration { + timePast: number, + wasAcceleration: boolean +} + interface PreAccelerate { swapRate: number suggestedRate: number suggestedRange: XYRange + earlyAcceleration?: EarlyAcceleration } export default class OrderPage extends BasePage { @@ -37,6 +43,8 @@ export default class OrderPage extends BasePage { secondTicker: number acceleratedRate: number refreshOnPopupClose: boolean + earlyAcceleration?: EarlyAcceleration + earlyAccelerationAlreadyDisplayed: boolean constructor (main: HTMLElement) { super() @@ -75,6 +83,9 @@ export default class OrderPage extends BasePage { Doc.bind(page.accelerateSubmit, 'click', () => { this.submitAccelerate() }) + Doc.bind(page.submitEarlyConfirm, 'click', () => { + this.submitAccelerate() + }) this.showAccelerationDiv() // If the user clicks outside of a form, it should close the page overlay. @@ -195,9 +206,11 @@ export default class OrderPage extends BasePage { this.showForm(page.accelerateForm) return } - Doc.hide(page.accelerateMsgDiv, page.preAccelerateErr, page.accelerateErr, page.feeEstimateDiv) - Doc.show(page.accelerateMainDiv, page.accelerateSuccess) + Doc.hide(page.accelerateMsgDiv, page.preAccelerateErr, page.accelerateErr, page.feeEstimateDiv, page.earlyAccelerationDiv) + Doc.show(page.accelerateMainDiv, page.accelerateSuccess, page.configureAccelerationDiv) const preAccelerate: PreAccelerate = res.preAccelerate + this.earlyAcceleration = preAccelerate.earlyAcceleration + this.earlyAccelerationAlreadyDisplayed = false page.accelerateAvgFeeRate.textContent = `${preAccelerate.swapRate} ${preAccelerate.suggestedRange.yUnit}` page.accelerateCurrentFeeRate.textContent = `${preAccelerate.suggestedRate} ${preAccelerate.suggestedRange.yUnit}` OrderUtil.setOptionTemplates(page) @@ -248,6 +261,24 @@ export default class OrderPage extends BasePage { orderID: order.id, newRate: this.acceleratedRate } + if (this.earlyAcceleration && !this.earlyAccelerationAlreadyDisplayed) { + page.recentAccelerationTime.textContent = `${Math.floor(this.earlyAcceleration.timePast / 60)}` + page.recentSwapTime.textContent = `${Math.floor(this.earlyAcceleration.timePast / 60)}` + if (this.earlyAcceleration.wasAcceleration) { + Doc.show(page.recentAccelerationMsg) + Doc.hide(page.recentSwapMsg) + page.recentAccelerationTime.textContent = `${Math.floor(this.earlyAcceleration.timePast / 60)}` + } else { + Doc.show(page.recentSwapMsg) + Doc.hide(page.recentAccelerationMsg) + page.recentSwapTime.textContent = `${Math.floor(this.earlyAcceleration.timePast / 60)}` + } + this.earlyAccelerationAlreadyDisplayed = true + Doc.hide(page.configureAccelerationDiv) + Doc.show(page.earlyAccelerationDiv) + this.earlyAccelerationAlreadyDisplayed = true + return + } page.acceleratePass.value = '' const loaded = app().loading(page.accelerateForm) const res = await postJSON('/api/accelerateorder', req) @@ -259,7 +290,8 @@ export default class OrderPage extends BasePage { Doc.show(page.accelerateMsgDiv, page.accelerateSuccess) } else { page.accelerateErr.textContent = `Error accelerating order: ${res.msg}` - Doc.show(page.accelerateErr) + Doc.hide(page.earlyAccelerationDiv) + Doc.show(page.accelerateErr, page.configureAccelerationDiv) } } diff --git a/client/webserver/site/src/localized_html/en-US/order.tmpl b/client/webserver/site/src/localized_html/en-US/order.tmpl index 7873eb9505..182bef25a2 100644 --- a/client/webserver/site/src/localized_html/en-US/order.tmpl +++ b/client/webserver/site/src/localized_html/en-US/order.tmpl @@ -229,27 +229,40 @@ Accelerate Order
- If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block. -
-
- Effective swap tx fee rate: -
-
- Current suggested fee rate: -
-
-
-
- Increasing the effective fee rate to will cost -
-
-
-
- - +
+ If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block. +
+
+ Effective swap tx fee rate: +
+
+ Current suggested fee rate: +
+
-
- +
+ Increasing the effective fee rate to will cost +
+
+
+
+ + +
+
+ +
+
+
+
+
Your latest acceleration was only minutes ago! Are you sure you want to accelerate?
+
Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?
+
+ It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions. +
+
+ +
diff --git a/client/webserver/site/src/localized_html/pl-PL/order.tmpl b/client/webserver/site/src/localized_html/pl-PL/order.tmpl index 460821fea6..66223206bc 100644 --- a/client/webserver/site/src/localized_html/pl-PL/order.tmpl +++ b/client/webserver/site/src/localized_html/pl-PL/order.tmpl @@ -229,27 +229,40 @@ Accelerate Order
- If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block. -
-
- Effective swap tx fee rate: -
-
- Current suggested fee rate: -
-
-
-
- Increasing the effective fee rate to will cost -
-
-
-
- - +
+ If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block. +
+
+ Effective swap tx fee rate: +
+
+ Current suggested fee rate: +
+
-
- +
+ Increasing the effective fee rate to will cost +
+
+
+
+ + +
+
+ +
+
+
+
+
Your latest acceleration was only minutes ago! Are you sure you want to accelerate?
+
Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?
+
+ It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions. +
+
+ +
diff --git a/client/webserver/site/src/localized_html/pt-BR/order.tmpl b/client/webserver/site/src/localized_html/pt-BR/order.tmpl index 37c4458067..aca7ca3d57 100644 --- a/client/webserver/site/src/localized_html/pt-BR/order.tmpl +++ b/client/webserver/site/src/localized_html/pt-BR/order.tmpl @@ -229,27 +229,40 @@ Accelerate Order
- If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block. -
-
- Effective swap tx fee rate: -
-
- Current suggested fee rate: -
-
-
-
- Increasing the effective fee rate to will cost -
-
-
-
- - +
+ If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block. +
+
+ Effective swap tx fee rate: +
+
+ Current suggested fee rate: +
+
-
- +
+ Increasing the effective fee rate to will cost +
+
+
+
+ + +
+
+ +
+
+
+
+
Your latest acceleration was only minutes ago! Are you sure you want to accelerate?
+
Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?
+
+ It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions. +
+
+ +
diff --git a/client/webserver/site/src/localized_html/zh-CN/order.tmpl b/client/webserver/site/src/localized_html/zh-CN/order.tmpl index f55509a7b7..a7e71cbbda 100644 --- a/client/webserver/site/src/localized_html/zh-CN/order.tmpl +++ b/client/webserver/site/src/localized_html/zh-CN/order.tmpl @@ -229,27 +229,40 @@ Accelerate Order
- If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block. -
-
- Effective swap tx fee rate: -
-
- Current suggested fee rate: -
-
-
-
- Increasing the effective fee rate to will cost -
-
-
-
- - +
+ If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block. +
+
+ Effective swap tx fee rate: +
+
+ Current suggested fee rate: +
+
-
- +
+ Increasing the effective fee rate to will cost +
+
+
+
+ + +
+
+ +
+
+
+
+
Your latest acceleration was only minutes ago! Are you sure you want to accelerate?
+
Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?
+
+ It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions. +
+
+ +
From f3ba8ba0fc0dfd2d64d4425e8f02c4c0dfc793e3 Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Mon, 9 May 2022 11:53:21 -0500 Subject: [PATCH 09/13] ExchangeWalletAccelerator --- client/asset/btc/btc.go | 80 +++++++++++++++++++++++++----------- client/asset/btc/btc_test.go | 5 ++- client/asset/interface.go | 7 +--- client/core/core_test.go | 4 -- 4 files changed, 60 insertions(+), 36 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 7b30fea243..461f085d10 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -270,8 +270,6 @@ type BTCCloneCFG struct { // output value) that doesn't depend on the serialized size of the output. // If ConstantDustLimit is zero, dexbtc.IsDust is used. ConstantDustLimit uint64 - // SupportsCPFP is true if the wallet supports child pays for parent. - SupportsCPFP bool } // outPoint is the hash and output index of a transaction output. @@ -593,7 +591,6 @@ type baseWallet struct { decodeAddr dexbtc.AddressDecoder stringAddr dexbtc.AddressStringer net dex.Network - supportsCPFP bool tipMtx sync.RWMutex currentTip *block @@ -617,9 +614,15 @@ type ExchangeWalletFullNode struct { *baseWallet } +// ExchangeWalletAccelerator implements the Accelerator interface on an +// ExchangeWAlletFullNode. +type ExchangeWalletAccelerator struct { + *ExchangeWalletFullNode +} + // Check that wallets satisfy their supported interfaces. var _ asset.Wallet = (*baseWallet)(nil) -var _ asset.Accelerator = (*baseWallet)(nil) +var _ asset.Accelerator = (*ExchangeWalletAccelerator)(nil) var _ asset.Rescanner = (*ExchangeWalletSPV)(nil) var _ asset.FeeRater = (*ExchangeWalletFullNode)(nil) var _ asset.LogFiler = (*ExchangeWalletSPV)(nil) @@ -720,14 +723,17 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, net dex.Network) (ass DefaultFallbackFee: defaultFee, DefaultFeeRateLimit: defaultFeeRateLimit, Segwit: true, - SupportsCPFP: true, } switch cfg.Type { case walletTypeSPV: return openSPVWallet(cloneCFG) case walletTypeRPC, walletTypeLegacy: - return BTCCloneWallet(cloneCFG) + rpcWallet, err := BTCCloneWallet(cloneCFG) + if err != nil { + return nil, err + } + return &ExchangeWalletAccelerator{rpcWallet}, nil default: return nil, fmt.Errorf("unknown wallet type %q", cfg.Type) } @@ -865,7 +871,6 @@ func newUnconnectedWallet(cfg *BTCCloneCFG, walletCfg *WalletConfig) (*baseWalle stringAddr: addrStringer, walletInfo: cfg.WalletInfo, net: cfg.Network, - supportsCPFP: cfg.SupportsCPFP, } if w.estimateFee == nil { @@ -2040,10 +2045,17 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B return tx, output, txFee + additionalFeesRequired, nil } -// CanAccelerate returns whether or not the wallet supports acceleration. -// Some of the BTC clones do not support it. -func (btc *baseWallet) CanAccelerate() bool { - return btc.supportsCPFP +// AccelerateOrder uses the Child-Pays-For-Parent technique to accelerate a +// chain of swap transactions and previous accelerations. It broadcasts a new +// transaction with a fee high enough so that the average fee of all the +// unconfirmed transactions in the chain and the new transaction will have +// an average fee rate of newFeeRate. requiredForRemainingSwaps is passed +// in to ensure that the new change coin will have enough funds to initiate +// the additional swaps that will be required to complete the order. +// +// The returned change coin may be nil, and should be checked before use. +func (btc *ExchangeWalletAccelerator) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { + return accelerateOrder(btc.baseWallet, swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) } // AccelerateOrder uses the Child-Pays-For-Parent technique to accelerate a @@ -2055,11 +2067,11 @@ func (btc *baseWallet) CanAccelerate() bool { // the additional swaps that will be required to complete the order. // // The returned change coin may be nil, and should be checked before use. -func (btc *baseWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { - if !btc.supportsCPFP { - return nil, "", fmt.Errorf("this wallet does not support acceleration") - } +func (btc *ExchangeWalletSPV) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { + return accelerateOrder(btc.baseWallet, swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) +} +func accelerateOrder(btc *baseWallet, swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { btc.fundingMtx.Lock() defer btc.fundingMtx.Unlock() @@ -2116,11 +2128,19 @@ func (btc *baseWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, // instead of broadcasting the acceleration transaction, it just returns // the amount of funds that will need to be spent in order to increase the // average fee rate to the desired amount. -func (btc *baseWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) { - if !btc.supportsCPFP { - return 0, fmt.Errorf("this wallet does not support acceleration") - } +func (btc *ExchangeWalletAccelerator) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) { + return accelerationEstimate(btc.baseWallet, swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) +} + +// AccelerationEstimate takes the same parameters as AccelerateOrder, but +// instead of broadcasting the acceleration transaction, it just returns +// the amount of funds that will need to be spent in order to increase the +// average fee rate to the desired amount. +func (btc *ExchangeWalletSPV) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) { + return accelerationEstimate(btc.baseWallet, swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) +} +func accelerationEstimate(btc *baseWallet, swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) { btc.fundingMtx.RLock() defer btc.fundingMtx.RUnlock() _, _, fee, err := btc.signedAccelerationTx(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) @@ -2184,15 +2204,27 @@ func tooEarlyToAccelerate(txs []*GetTransactionResult, accelerationCoins []dex.B // the user a good amount of flexibility in determining the post acceleration // effective fee rate, but still not allowing them to pick something // outrageously high. -func (btc *baseWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { +func (btc *ExchangeWalletAccelerator) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { + return preAccelerate(btc.baseWallet, swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion) +} + +// PreAccelerate returns the current average fee rate of the unmined swap +// initiation and acceleration transactions, and also returns a suggested +// range that the fee rate should be increased to in order to expedite mining. +// The feeSuggestion argument is the current prevailing network rate. It is +// used to help determine the suggestedRange, which is a range meant to give +// the user a good amount of flexibility in determining the post acceleration +// effective fee rate, but still not allowing them to pick something +// outrageously high. +func (btc *ExchangeWalletSPV) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { + return preAccelerate(btc.baseWallet, swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion) +} + +func preAccelerate(btc *baseWallet, swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { makeError := func(err error) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { return 0, asset.XYRange{}, nil, err } - if !btc.supportsCPFP { - return makeError(fmt.Errorf("this wallet does not support acceleration")) - } - changeTxHash, changeVout, err := decodeCoinID(changeCoin) if err != nil { return makeError(err) diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index 218fc14273..895e84b124 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -626,7 +626,6 @@ func tNewWallet(segwit bool, walletType string) (*ExchangeWalletFullNode, *testD DefaultFallbackFee: defaultFee, DefaultFeeRateLimit: defaultFeeRateLimit, Segwit: segwit, - SupportsCPFP: true, } var wallet *ExchangeWalletFullNode @@ -3120,12 +3119,14 @@ func TestAccelerateOrder(t *testing.T) { } func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { - wallet, node, shutdown, err := tNewWallet(segwit, walletType) + w, node, shutdown, err := tNewWallet(segwit, walletType) defer shutdown() if err != nil { t.Fatal(err) } + wallet := &ExchangeWalletAccelerator{w} + var blockHash100 chainhash.Hash copy(blockHash100[:], encode.RandomBytes(32)) node.verboseBlocks[blockHash100.String()] = &msgBlockWithHeight{height: 100, msgBlock: &wire.MsgBlock{ diff --git a/client/asset/interface.go b/client/asset/interface.go index ce2cb44531..f5b76288f9 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -66,7 +66,7 @@ func DetermineWalletTraits(w Wallet) (t WalletTrait) { if _, is := w.(FeeRater); is { t |= WalletTraitFeeRater } - if a, is := w.(Accelerator); is && a.CanAccelerate() { + if _, is := w.(Accelerator); is { t |= WalletTraitAccelerator } return t @@ -411,11 +411,6 @@ type Accelerator interface { // the user's previous acceleration on this order or the earliest // unmined transaction in this order happened very recently. PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, XYRange, *EarlyAcceleration, error) - // CanAccelerate returns whether or not the wallet can accelerate. Ideally - // we would know this just by whether or not the wallet implements this - // interface, but since all bitcoin clones use the same implementation - // and some of them do not support acceleration, this function is needed. - CanAccelerate() bool } // TokenMaster is implemented by assets which support degenerate tokens. diff --git a/client/core/core_test.go b/client/core/core_test.go index a97adbf68e..5e2540c801 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -950,10 +950,6 @@ func (w *TXCWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Byte return w.accelerationEstimate, nil } -func (w *TXCWallet) CanAccelerate() bool { - return true -} - type TAccountLocker struct { *TXCWallet reserveNRedemptions uint64 From 95dabfe5d9e69a3743bd21d18061c06eb805e228 Mon Sep 17 00:00:00 2001 From: martonp Date: Mon, 16 May 2022 20:01:02 +0700 Subject: [PATCH 10/13] Display button to accelerate on markets page --- client/webserver/locales/en-us.go | 2 +- client/webserver/site/src/css/icons.scss | 4 + client/webserver/site/src/css/market.scss | 5 + client/webserver/site/src/font/icomoon.svg | 1 + client/webserver/site/src/font/icomoon.ttf | Bin 5508 -> 5948 bytes client/webserver/site/src/font/icomoon.woff | Bin 5584 -> 6024 bytes client/webserver/site/src/html/forms.tmpl | 72 +++++++ client/webserver/site/src/html/markets.tmpl | 5 +- client/webserver/site/src/html/order.tmpl | 82 +------- client/webserver/site/src/js/app.ts | 23 +++ client/webserver/site/src/js/forms.ts | 171 ++++++++++++++- client/webserver/site/src/js/markets.ts | 43 +++- client/webserver/site/src/js/order.ts | 194 +++--------------- client/webserver/site/src/js/registry.ts | 1 + .../site/src/localized_html/en-US/forms.tmpl | 72 +++++++ .../src/localized_html/en-US/markets.tmpl | 5 +- .../site/src/localized_html/en-US/order.tmpl | 82 +------- .../site/src/localized_html/pl-PL/forms.tmpl | 72 +++++++ .../src/localized_html/pl-PL/markets.tmpl | 5 +- .../site/src/localized_html/pl-PL/order.tmpl | 82 +------- .../site/src/localized_html/pt-BR/forms.tmpl | 72 +++++++ .../src/localized_html/pt-BR/markets.tmpl | 5 +- .../site/src/localized_html/pt-BR/order.tmpl | 82 +------- .../site/src/localized_html/zh-CN/forms.tmpl | 72 +++++++ .../src/localized_html/zh-CN/markets.tmpl | 5 +- .../site/src/localized_html/zh-CN/order.tmpl | 82 +------- 26 files changed, 683 insertions(+), 556 deletions(-) diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index 5250aae595..d2de89d46e 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -216,7 +216,7 @@ var EnUS = map[string]string{ "max_fee_conditions": "This is the most you would ever pay in fees on your swap. Fees are normally assessed at a fraction of this rate. The maximum is not subject to changes once your order is placed.", "wallet_logs": "Wallet Logs", "accelerate_order": "Accelerate Order", - "acceleration_text": "If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block.", + "acceleration_text": "If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. The effective fee rate of your swap transactions will become the rate you select below. Select a rate that is enough to be included in the next block. Consult a block explorer to be sure.", "effective_swap_tx_rate": "Effective swap tx fee rate", "current_fee": "Current suggested fee rate", "accelerate_success": `Successfully submitted transaction: `, diff --git a/client/webserver/site/src/css/icons.scss b/client/webserver/site/src/css/icons.scss index 46e76949bd..65d7e86436 100644 --- a/client/webserver/site/src/css/icons.scss +++ b/client/webserver/site/src/css/icons.scss @@ -56,6 +56,10 @@ content: "\e902"; } +.ico-rocket::before { + content: "\e9a5"; +} + .ico-profile::before { content: "\e903"; } diff --git a/client/webserver/site/src/css/market.scss b/client/webserver/site/src/css/market.scss index fe2773b4a1..96bbcced88 100644 --- a/client/webserver/site/src/css/market.scss +++ b/client/webserver/site/src/css/market.scss @@ -696,6 +696,11 @@ div.numorders { font-size: 12px; cursor: pointer; } + + .ico-rocket { + font-size: 12px; + cursor: pointer; + } } #vDetailPane { diff --git a/client/webserver/site/src/font/icomoon.svg b/client/webserver/site/src/font/icomoon.svg index 1b4f26b54c..2f280e1ee9 100644 --- a/client/webserver/site/src/font/icomoon.svg +++ b/client/webserver/site/src/font/icomoon.svg @@ -25,6 +25,7 @@ + diff --git a/client/webserver/site/src/font/icomoon.ttf b/client/webserver/site/src/font/icomoon.ttf index fe02edc3445f234e6a82610a7975ef7065d89aca..6f261545d143487aa8847e71b2247d5f873ae7df 100644 GIT binary patch delta 861 zcmZ8fT}V@57=FK<$bCUL${snY#ep&-(_n%UW78R5f#R!&AZg%7Q;|l=9hInNvH#dt}kN62@#Z>;vr0c_jtB79$ zw7i*~$enjgJZc3n&7&TjMnd7I(}kGob=?<#t_B{Zoka#X_?`*;V!idyT!!YFSmONDh~ZW_ojt z_|;1^$fY8^hX-q+6EZM~y#!ec<0G6?&Ug|eJUr)Qw1kl} zgl*7)|9DTs)pO8H!dQnjo8+96mle}1;iU{W5{Zm(nea-wdej#V&m|I~m`Kcp!@jIb z5QYQ6P$(D}76g~aorlVTGP#OU0IjSc8phH@HU%^>=?|ZfYsKPjS51qR;SbU z3PLp7X0rxNW`jYaX=$SArWTFHurF^EcEjd-%@O*nNCPE;1wctS%3pshi;}SQ_X)To z#3(CQ%jL4uF&>L`R$r41mZDmAFP$4M{(1e5}(R(%KKv+vI;{c7a yyp%WEN<|}Gx@Gh@UNNK0DzmNFR{T*0l_!+=o6%;4r@++hveG59ar3iro%{u?D8OO> delta 396 zcmdm^*P>m|z{tSBz|GLWz|3IaAFOZ0KbQ3%P-G7fCnV=47VMq4cP|42qYRK=lb%>y z0Hg(g{24%+BR!`wE$ZL(l|cRi1_nWmjMT&wxrm^53=BdifbwP;Kmm4hW?u#dVIY@5 zB_p?_B9eje9+0mBuU;+9^;U(|O>X#Ex)~==WE9=JlF@)?vnam>%jC7f z=anMubw%U(ZN4&avw+RHJEwgOhykJh`>-%ho+lz91(N*#pM?=VuD&_kHhY=FRK{*9Vwe z`H>MofQ?&+uGX5ae?m-sACZsxEUOwrxmsETKozWQs@VSV;`&^vrgA^AuRVcjZvOT) z%pCz(-l_0@e0rxcTe=Fs$T%jU!ae(}w}MH`{lUIhg?5n4SE@_P05%8q=T*!A`Zm8% zEzGKYNINeitEFY!*d}A&)dCMMZ zx7d5^UG^qRvwBPrk|&`=^8 z;hc!Wir^0+zrcGa8c`Ywv28m1AMbBx4V_L#FEV8!A21pW#sQx1FM1u0^MW3m;Ba`+ zp4rBP?G9%#A&M!wyPHmlVxs7D*u#v?Y&IG#og~?5F&fRsns#9y>@f6M@{&XXApr-V zLp(9q8WyKRY->JYUmRIG}$S3+blu!(li1jNdz=y1++R7N$8t1Xe^RS zmE0~_MiP?h!yLUHkl+QRj$|B`bP3m6y}Gl1eXAiQ_t-o5FG z#UQaKKt2Z)3#8{%rUAv?Ffa&efN<2m>nk%-6H^!%ghhaA%s^N!BIsQPP!K5Q1LUiK zFuOUkZ$@rO1yBrVCIe7CLrNqADN0LF?8 ztMd|bQ-NYYOBvfhc%~QU?}Gf|5}q(1~DEn4xnyEU?7QZ z{>o^;vpJF9f@Shg;qxJp_PV0+{5D@1xLLsF+?~_D2E>5S|9w~(nSFs=4hAN$G|;_3 zcNzZ&dJv@k|9=)nkO&HpoGc^i!2*<4m|P%gFMEjX7rP#Z9!CmC1IHAwVo8RT4D4Vn POp_ln3U7Wax|9(BwX$tU diff --git a/client/webserver/site/src/html/forms.tmpl b/client/webserver/site/src/html/forms.tmpl index d64b978352..bc6f677ade 100644 --- a/client/webserver/site/src/html/forms.tmpl +++ b/client/webserver/site/src/html/forms.tmpl @@ -372,6 +372,78 @@
{{end}} +{{define "accelerateForm"}} +{{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+
+ [[[accelerate_order]]] +
+
+
+ [[[acceleration_text]]] +
+
+ [[[effective_swap_tx_rate]]]: +
+
+ [[[current_fee]]]: +
+
+
+
+ [[[acceleration_cost_msg]]] +
+
+
+
+ + +
+
+ +
+
+
+
+
[[[recent_acceleration_msg]]]
+
[[[recent_swap_msg]]]
+
+ [[[early_acceleration_help_msg]]] +
+
+ +
+
+
+
+
+
+
+ +
+
+
+ +
+
+ + + + + + + + +
+
+
+
+
+ [[[accelerate_success]]] +
+
+{{end}} + {{define "waitingForWalletForm"}}
diff --git a/client/webserver/site/src/html/markets.tmpl b/client/webserver/site/src/html/markets.tmpl index 3b309d48da..a58b76381c 100644 --- a/client/webserver/site/src/html/markets.tmpl +++ b/client/webserver/site/src/html/markets.tmpl @@ -389,6 +389,7 @@ + @@ -653,7 +654,9 @@
{{template "cancelOrderForm" .}}
- +
+ {{template "accelerateForm" .}} +
diff --git a/client/webserver/site/src/html/order.tmpl b/client/webserver/site/src/html/order.tmpl index d721e3ff8e..20e65226ab 100644 --- a/client/webserver/site/src/html/order.tmpl +++ b/client/webserver/site/src/html/order.tmpl @@ -84,12 +84,14 @@
[[[Age]]]
-
-
[[[accelerate]]]
-
-
{{- /* END DATA CARDS */ -}} + {{- /* ACTIONS */ -}} +
[[[Actions]]]
+
+ +
+ {{- /* MATCHES */ -}}
[[[Matches]]]
@@ -223,79 +225,9 @@ {{template "cancelOrderForm" .}}
- {{$passwordIsCached := .UserInfo.PasswordIsCached}} -
-
- [[[accelerate_order]]] -
-
-
- [[[acceleration_text]]] -
-
- [[[effective_swap_tx_rate]]]: -
-
- [[[current_fee]]]: -
-
-
-
- [[[acceleration_cost_msg]]] -
-
-
-
- - -
-
- -
-
-
-
-
[[[recent_acceleration_msg]]]
-
[[[recent_swap_msg]]]
-
- [[[early_acceleration_help_msg]]] -
-
- -
-
-
-
-
-
-
-
- [[[accelerate_success]]] -
-
+ {{template "accelerateForm" .}}
- -
-
- -
-
-
- -
-
- - - - - - - - -
-
-
{{template "bottom"}} {{end}} diff --git a/client/webserver/site/src/js/app.ts b/client/webserver/site/src/js/app.ts index d5fcc92b1c..debc640c41 100644 --- a/client/webserver/site/src/js/app.ts +++ b/client/webserver/site/src/js/app.ts @@ -754,6 +754,29 @@ export default class Application { return null } + /* + * canAccelerateOrder returns true if the "from" wallet of the order + * supports acceleration, and if the order has unconfirmed swap + * transactions. + */ + canAccelerateOrder (order: Order): boolean { + const walletTraitAccelerator = 1 << 4 + let fromAssetID + if (order.sell) fromAssetID = order.baseID + else fromAssetID = order.quoteID + const wallet = this.walletMap[fromAssetID] + if (!wallet || !(wallet.traits & walletTraitAccelerator)) return false + if (order.matches) { + for (let i = 0; i < order.matches.length; i++) { + const match = order.matches[i] + if (match.swap && match.swap.confs && match.swap.confs.count === 0) { + return true + } + } + } + return false + } + /* * unitInfo fetches unit info [dex.UnitInfo] for the asset. If xc * [core.Exchange] is provided, and this is not a SupportedAsset, the UnitInfo diff --git a/client/webserver/site/src/js/forms.ts b/client/webserver/site/src/js/forms.ts index df8f8b3c32..1cbef680ed 100644 --- a/client/webserver/site/src/js/forms.ts +++ b/client/webserver/site/src/js/forms.ts @@ -2,7 +2,7 @@ import Doc from './doc' import { postJSON } from './http' import State from './state' import * as intl from './locales' -import { RateEncodingFactor } from './orderutil' +import * as OrderUtil from './orderutil' import { app, PasswordCache, @@ -15,7 +15,9 @@ import { UnitInfo, FeeAsset, WalletState, - WalletBalance + WalletBalance, + Order, + XYRange } from './registry' interface ConfigOptionInput extends HTMLInputElement { @@ -610,7 +612,7 @@ export class FeeAssetSelectionForm { if (mkt.spot) { Doc.show(marketTmpl.quoteLotSize) const r = cFactor(quoteUnitInfo) / cFactor(baseUnitInfo) - const quoteLot = mkt.lotsize * mkt.spot.rate / RateEncodingFactor * r + const quoteLot = mkt.lotsize * mkt.spot.rate / OrderUtil.RateEncodingFactor * r const s = Doc.formatCoinValue(quoteLot, quoteUnitInfo) marketTmpl.quoteLotSize.textContent = `(~${s} ${quoteSymbol})` } @@ -907,6 +909,169 @@ export class UnlockWalletForm { } } +interface EarlyAcceleration { + timePast: number, + wasAcceleration: boolean +} + +interface PreAccelerate { + swapRate: number + suggestedRate: number + suggestedRange: XYRange + earlyAcceleration?: EarlyAcceleration +} + +/* + * AccelerateOrderForm is used to submit an acceleration request for an order. + */ +export class AccelerateOrderForm { + form: HTMLElement + page: Record + order: Order + acceleratedRate: number + earlyAcceleration?: EarlyAcceleration + currencyUnit: string + success: () => void + + constructor (form: HTMLElement, success: () => void) { + this.form = form + this.success = success + const page = this.page = Doc.idDescendants(form) + + Doc.bind(page.accelerateSubmit, 'click', () => { + this.submit() + }) + Doc.bind(page.submitEarlyConfirm, 'click', () => { + this.sendAccelerateRequest() + }) + } + + /* + * displayEarlyAccelerationMsg displays a message asking for confirmation + * when the user tries to submit an acceleration transaction very soon after + * the swap transaction was broadcast, or very soon after a previous + * acceleration. + */ + displayEarlyAccelerationMsg () { + const page = this.page + // this is checked in submit, but another check is needed for ts compiler + if (!this.earlyAcceleration) return + page.recentAccelerationTime.textContent = `${Math.floor(this.earlyAcceleration.timePast / 60)}` + page.recentSwapTime.textContent = `${Math.floor(this.earlyAcceleration.timePast / 60)}` + if (this.earlyAcceleration.wasAcceleration) { + Doc.show(page.recentAccelerationMsg) + Doc.hide(page.recentSwapMsg) + page.recentAccelerationTime.textContent = `${Math.floor(this.earlyAcceleration.timePast / 60)}` + } else { + Doc.show(page.recentSwapMsg) + Doc.hide(page.recentAccelerationMsg) + page.recentSwapTime.textContent = `${Math.floor(this.earlyAcceleration.timePast / 60)}` + } + Doc.hide(page.configureAccelerationDiv, page.accelerateErr) + Doc.show(page.earlyAccelerationDiv) + } + + // sendAccelerateRequest makes an accelerateorder request to the client + // backend. + async sendAccelerateRequest () { + const order = this.order + const page = this.page + const req = { + pw: page.acceleratePass.value, + orderID: order.id, + newRate: this.acceleratedRate + } + page.acceleratePass.value = '' + const loaded = app().loading(page.accelerateMainDiv) + const res = await postJSON('/api/accelerateorder', req) + loaded() + if (app().checkResponse(res)) { + page.accelerateTxID.textContent = res.txID + Doc.hide(page.accelerateMainDiv, page.preAccelerateErr, page.accelerateErr) + Doc.show(page.accelerateMsgDiv, page.accelerateSuccess) + this.success() + } else { + page.accelerateErr.textContent = `Error accelerating order: ${res.msg}` + Doc.hide(page.earlyAccelerationDiv) + Doc.show(page.accelerateErr, page.configureAccelerationDiv) + } + } + + // submit is called when the submit button is clicked. + async submit () { + if (this.earlyAcceleration) { + this.displayEarlyAccelerationMsg() + } else { + this.sendAccelerateRequest() + } + } + + // refresh should be called before the form is displayed. It makes a + // preaccelerate request to the client backend and sets up the form + // based on the results. + async refresh (order: Order) { + const page = this.page + this.order = order + const res = await postJSON('/api/preaccelerate', order.id) + if (!app().checkResponse(res)) { + page.preAccelerateErr.textContent = `Error accelerating order: ${res.msg}` + Doc.hide(page.accelerateMainDiv, page.accelerateSuccess) + Doc.show(page.accelerateMsgDiv, page.preAccelerateErr) + return + } + Doc.hide(page.accelerateMsgDiv, page.preAccelerateErr, page.accelerateErr, page.feeEstimateDiv, page.earlyAccelerationDiv) + Doc.show(page.accelerateMainDiv, page.accelerateSuccess, page.configureAccelerationDiv) + const preAccelerate: PreAccelerate = res.preAccelerate + this.earlyAcceleration = preAccelerate.earlyAcceleration + this.currencyUnit = preAccelerate.suggestedRange.yUnit + page.accelerateAvgFeeRate.textContent = `${preAccelerate.swapRate} ${preAccelerate.suggestedRange.yUnit}` + page.accelerateCurrentFeeRate.textContent = `${preAccelerate.suggestedRate} ${preAccelerate.suggestedRange.yUnit}` + OrderUtil.setOptionTemplates(page) + this.acceleratedRate = preAccelerate.suggestedRange.start.y + const selected = () => { /* do nothing */ } + const roundY = true + const updateRate = (_: number, newY: number) => { this.acceleratedRate = newY } + const rangeHandler = new OrderUtil.XYRangeHandler(preAccelerate.suggestedRange, + preAccelerate.suggestedRange.start.x, updateRate, () => this.updateAccelerationEstimate(), selected, roundY) + Doc.empty(page.sliderContainer) + page.sliderContainer.appendChild(rangeHandler.control) + this.updateAccelerationEstimate() + } + + // updateAccelerationEstimate makes an accelerateestimate request to the + // client backend using the curretly selected rate on the slider, and + // displays the results. + async updateAccelerationEstimate () { + const page = this.page + const order = this.order + const req = { + orderID: order.id, + newRate: this.acceleratedRate + } + const loaded = app().loading(page.sliderContainer) + const res = await postJSON('/api/accelerationestimate', req) + loaded() + if (!app().checkResponse(res)) { + page.accelerateErr.textContent = `Error estimating acceleration fee: ${res.msg}` + Doc.show(page.accelerateErr) + return + } + page.feeRateEstimate.textContent = `${this.acceleratedRate} ${this.currencyUnit}` + let assetID + let assetSymbol + if (order.sell) { + assetID = order.baseID + assetSymbol = order.baseSymbol + } else { + assetID = order.quoteID + assetSymbol = order.quoteSymbol + } + const unitInfo = app().unitInfo(assetID) + page.feeEstimate.textContent = `${res.fee / unitInfo.conventional.conversionFactor} ${assetSymbol}` + Doc.show(page.feeEstimateDiv) + } +} + /* DEXAddressForm accepts a DEX address and performs account discovery. */ export class DEXAddressForm { form: HTMLElement diff --git a/client/webserver/site/src/js/markets.ts b/client/webserver/site/src/js/markets.ts index 12d6c7851e..3140395a59 100644 --- a/client/webserver/site/src/js/markets.ts +++ b/client/webserver/site/src/js/markets.ts @@ -12,7 +12,7 @@ import { DepthMarker } from './charts' import { postJSON } from './http' -import { NewWalletForm, UnlockWalletForm, bind as bindForm } from './forms' +import { NewWalletForm, UnlockWalletForm, AccelerateOrderForm, bind as bindForm } from './forms' import * as OrderUtil from './orderutil' import ws from './ws' import * as intl from './locales' @@ -173,6 +173,7 @@ export default class MarketsPage extends BasePage { keyup: (e: KeyboardEvent) => void secondTicker: number candlesLoading: LoadTracker | null + accelerateOrderForm: AccelerateOrderForm constructor (main: HTMLElement, data: any) { super() @@ -210,6 +211,11 @@ export default class MarketsPage extends BasePage { } this.candleChart = new CandleChart(page.marketChart, candleReporters) + const success = () => { /* do nothing */ } + // Do not call cleanTemplates before creating the AccelerateOrderForm + this.accelerateOrderForm = new AccelerateOrderForm(page.accelerateForm, success) + Doc.cleanTemplates(page.rangeOptTmpl) + // TODO: Store user's state and reload last known configuration. this.candleChart.hide() this.currentChart = depthChart @@ -1130,6 +1136,16 @@ export default class MarketsPage extends BasePage { this.showCancel(row, ord.id) }) } + + const accelerateBttn = Doc.tmplElement(row, 'accelerateBttn') + bind(accelerateBttn, 'click', e => { + e.stopPropagation() + this.showAccelerate(ord) + }) + if (app().canAccelerateOrder(ord)) { + Doc.show(accelerateBttn) + } + const side = Doc.tmplElement(row, 'side') side.classList.add(ord.sell ? 'sellcolor' : 'buycolor') const link = Doc.tmplElement(row, 'link') @@ -1320,7 +1336,8 @@ export default class MarketsPage extends BasePage { async showForm (form: HTMLElement) { this.currentForm = form const page = this.page - Doc.hide(page.unlockWalletForm, page.verifyForm, page.newWalletForm, page.cancelForm, page.vDetailPane) + Doc.hide(page.unlockWalletForm, page.verifyForm, page.newWalletForm, + page.cancelForm, page.vDetailPane, page.accelerateForm) form.style.right = '10000px' Doc.show(page.forms, form) const shift = (page.forms.offsetWidth + form.offsetWidth) / 2 @@ -1591,6 +1608,14 @@ export default class MarketsPage extends BasePage { } } + /* showAccelerate shows the accelerate order form. */ + showAccelerate (order: Order) { + const loaded = app().loading(this.main) + this.accelerateOrderForm.refresh(order) + loaded() + this.showForm(this.page.accelerateForm) + } + /* showCreate shows the new wallet creation form. */ showCreate (asset: SupportedAsset) { const page = this.page @@ -1669,14 +1694,12 @@ export default class MarketsPage extends BasePage { if (!metaOrder) return this.refreshActiveOrders() const oldStatus = metaOrder.status metaOrder.order = order - const bttn = Doc.tmplElement(metaOrder.row, 'cancelBttn') - if (note.topic === 'MissedCancel') { - Doc.show(bttn) - } - if (order.filled === order.qty) { - // Remove the cancellation button. - Doc.hide(bttn) - } + const cancelBttn = Doc.tmplElement(metaOrder.row, 'cancelBttn') + if (note.topic === 'MissedCancel') Doc.show(cancelBttn) + if (order.filled === order.qty) Doc.hide(cancelBttn) + const accelerateBttn = Doc.tmplElement(metaOrder.row, 'accelerateBttn') + if (app().canAccelerateOrder(order)) Doc.show(accelerateBttn) + else Doc.hide(accelerateBttn) this.updateUserOrderRow(metaOrder.row, order) // Only reset markers if there is a change, since the chart is redrawn. if ((oldStatus === OrderUtil.StatusEpoch && order.status === OrderUtil.StatusBooked) || diff --git a/client/webserver/site/src/js/order.ts b/client/webserver/site/src/js/order.ts index e65960e134..39d3b0e5bf 100644 --- a/client/webserver/site/src/js/order.ts +++ b/client/webserver/site/src/js/order.ts @@ -1,7 +1,7 @@ import Doc from './doc' import BasePage from './basepage' import * as OrderUtil from './orderutil' -import { bind as bindForm } from './forms' +import { bind as bindForm, AccelerateOrderForm } from './forms' import { postJSON } from './http' import * as intl from './locales' import { @@ -11,8 +11,7 @@ import { OrderNote, MatchNote, Match, - Coin, - XYRange + Coin } from './registry' const Mainnet = 0 @@ -23,28 +22,14 @@ const animationLength = 500 let net: number -interface EarlyAcceleration { - timePast: number, - wasAcceleration: boolean -} - -interface PreAccelerate { - swapRate: number - suggestedRate: number - suggestedRange: XYRange - earlyAcceleration?: EarlyAcceleration -} - export default class OrderPage extends BasePage { orderID: string order: Order page: Record currentForm: HTMLElement secondTicker: number - acceleratedRate: number refreshOnPopupClose: boolean - earlyAcceleration?: EarlyAcceleration - earlyAccelerationAlreadyDisplayed: boolean + accelerateOrderForm: AccelerateOrderForm constructor (main: HTMLElement) { super() @@ -76,17 +61,17 @@ export default class OrderPage extends BasePage { }) } - Doc.cleanTemplates(page.rangeOptTmpl) Doc.bind(page.accelerateBttn, 'click', () => { this.showAccelerateForm() }) - Doc.bind(page.accelerateSubmit, 'click', () => { - this.submitAccelerate() - }) - Doc.bind(page.submitEarlyConfirm, 'click', () => { - this.submitAccelerate() - }) - this.showAccelerationDiv() + + this.showAccelerationButton() + const success = () => { + this.refreshOnPopupClose = true + } + // Do not call cleanTemplates before creating the AccelerateOrderForm + this.accelerateOrderForm = new AccelerateOrderForm(page.accelerateForm, success) + Doc.cleanTemplates(page.rangeOptTmpl) // If the user clicks outside of a form, it should close the page overlay. Doc.bind(page.forms, 'mousedown', (e: MouseEvent) => { @@ -160,141 +145,6 @@ export default class OrderPage extends BasePage { form.style.right = '0px' } - /* - * showAccelerationDiv shows the acceleration button if the "from" asset's - * wallet supports acceleration and the order has unconfirmed swap transactions - */ - showAccelerationDiv () { - const order = this.order - if (!order) return - const page = this.page - const canAccelerateOrder: () => boolean = () => { - const walletTraitAccelerator = 1 << 4 - let fromAssetID - if (order.sell) fromAssetID = order.baseID - else fromAssetID = order.quoteID - const wallet = app().walletMap[fromAssetID] - if (!wallet || !(wallet.traits & walletTraitAccelerator)) return false - if (order.matches) { - for (let i = 0; i < order.matches.length; i++) { - const match = order.matches[i] - if (match.swap && match.swap.confs && match.swap.confs.count === 0) { - return true - } - } - } - return false - } - if (canAccelerateOrder()) Doc.show(page.accelerateDiv) - else Doc.hide(page.accelerateDiv) - } - - /* showAccelerateForm shows a form to accelerate an order */ - async showAccelerateForm () { - const page = this.page - const order = this.order - while (page.sliderContainer.firstChild) { - page.sliderContainer.removeChild(page.sliderContainer.firstChild) - } - const loaded = app().loading(page.accelerateDiv) - const res = await postJSON('/api/preaccelerate', order.id) - loaded() - if (!app().checkResponse(res)) { - page.preAccelerateErr.textContent = `Error accelerating order: ${res.msg}` - Doc.hide(page.accelerateMainDiv, page.accelerateSuccess) - Doc.show(page.accelerateMsgDiv, page.preAccelerateErr) - this.showForm(page.accelerateForm) - return - } - Doc.hide(page.accelerateMsgDiv, page.preAccelerateErr, page.accelerateErr, page.feeEstimateDiv, page.earlyAccelerationDiv) - Doc.show(page.accelerateMainDiv, page.accelerateSuccess, page.configureAccelerationDiv) - const preAccelerate: PreAccelerate = res.preAccelerate - this.earlyAcceleration = preAccelerate.earlyAcceleration - this.earlyAccelerationAlreadyDisplayed = false - page.accelerateAvgFeeRate.textContent = `${preAccelerate.swapRate} ${preAccelerate.suggestedRange.yUnit}` - page.accelerateCurrentFeeRate.textContent = `${preAccelerate.suggestedRate} ${preAccelerate.suggestedRange.yUnit}` - OrderUtil.setOptionTemplates(page) - this.acceleratedRate = preAccelerate.suggestedRange.start.y - const updated = (_: number, newY: number) => { this.acceleratedRate = newY } - const changed = async () => { - const req = { - orderID: order.id, - newRate: this.acceleratedRate - } - const loaded = app().loading(page.sliderContainer) - const res = await postJSON('/api/accelerationestimate', req) - loaded() - if (!app().checkResponse(res)) { - page.accelerateErr.textContent = `Error estimating acceleration fee: ${res.msg}` - Doc.show(page.accelerateErr) - return - } - page.feeRateEstimate.textContent = `${this.acceleratedRate} ${preAccelerate.suggestedRange.yUnit}` - let assetID - let assetSymbol - if (order.sell) { - assetID = order.baseID - assetSymbol = order.baseSymbol - } else { - assetID = order.quoteID - assetSymbol = order.quoteSymbol - } - const unitInfo = app().unitInfo(assetID) - page.feeEstimate.textContent = `${res.fee / unitInfo.conventional.conversionFactor} ${assetSymbol}` - Doc.show(page.feeEstimateDiv) - } - const selected = () => { /* do nothing */ } - const roundY = true - const rangeHandler = new OrderUtil.XYRangeHandler(preAccelerate.suggestedRange, - preAccelerate.suggestedRange.start.x, updated, changed, selected, roundY) - page.sliderContainer.appendChild(rangeHandler.control) - changed() - this.showForm(page.accelerateForm) - } - - /* submitAccelerate sends a request to accelerate an order */ - async submitAccelerate () { - const order = this.order - const page = this.page - const req = { - pw: page.acceleratePass.value, - orderID: order.id, - newRate: this.acceleratedRate - } - if (this.earlyAcceleration && !this.earlyAccelerationAlreadyDisplayed) { - page.recentAccelerationTime.textContent = `${Math.floor(this.earlyAcceleration.timePast / 60)}` - page.recentSwapTime.textContent = `${Math.floor(this.earlyAcceleration.timePast / 60)}` - if (this.earlyAcceleration.wasAcceleration) { - Doc.show(page.recentAccelerationMsg) - Doc.hide(page.recentSwapMsg) - page.recentAccelerationTime.textContent = `${Math.floor(this.earlyAcceleration.timePast / 60)}` - } else { - Doc.show(page.recentSwapMsg) - Doc.hide(page.recentAccelerationMsg) - page.recentSwapTime.textContent = `${Math.floor(this.earlyAcceleration.timePast / 60)}` - } - this.earlyAccelerationAlreadyDisplayed = true - Doc.hide(page.configureAccelerationDiv) - Doc.show(page.earlyAccelerationDiv) - this.earlyAccelerationAlreadyDisplayed = true - return - } - page.acceleratePass.value = '' - const loaded = app().loading(page.accelerateForm) - const res = await postJSON('/api/accelerateorder', req) - loaded() - if (app().checkResponse(res)) { - this.refreshOnPopupClose = true - page.accelerateTxID.textContent = res.txID - Doc.hide(page.accelerateMainDiv, page.preAccelerateErr, page.accelerateErr) - Doc.show(page.accelerateMsgDiv, page.accelerateSuccess) - } else { - page.accelerateErr.textContent = `Error accelerating order: ${res.msg}` - Doc.hide(page.earlyAccelerationDiv) - Doc.show(page.accelerateErr, page.configureAccelerationDiv) - } - } - /* submitCancel submits a cancellation for the order. */ async submitCancel () { // this will be the page.cancelSubmit button (evt.currentTarget) @@ -314,6 +164,26 @@ export default class OrderPage extends BasePage { order.cancelling = true } + /* + * showAccelerationButton shows the acceleration button if the order can + * be accelerated. + */ + showAccelerationButton () { + const order = this.order + if (!order) return + const page = this.page + if (app().canAccelerateOrder(order)) Doc.show(page.accelerateBttn, page.actionsLabel) + else Doc.hide(page.accelerateBttn, page.actionsLabel) + } + + /* showAccelerateForm shows a form to accelerate an order */ + async showAccelerateForm () { + const loaded = app().loading(this.page.accelerateBttn) + this.accelerateOrderForm.refresh(this.order) + loaded() + this.showForm(this.page.accelerateForm) + } + /* * handleOrderNote is the handler for the 'order'-type notification, which are * used to update an order's status. @@ -328,7 +198,7 @@ export default class OrderPage extends BasePage { page.status.textContent = OrderUtil.statusString(order) } for (const m of order.matches || []) this.processMatch(m) - this.showAccelerationDiv() + this.showAccelerationButton() } /* handleMatchNote handles a 'match' notification. */ diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts index 00128c8a26..c8092fffec 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -484,6 +484,7 @@ export interface Application { orders (host: string, mktID: string): Order[] haveAssetOrders (assetID: number): boolean order (oid: string): Order | null + canAccelerateOrder(order: Order): boolean unitInfo (assetID: number, xc?: Exchange): UnitInfo conventionalRate (baseID: number, quoteID: number, encRate: number): number walletDefinition (assetID: number, walletType: string): WalletDefinition diff --git a/client/webserver/site/src/localized_html/en-US/forms.tmpl b/client/webserver/site/src/localized_html/en-US/forms.tmpl index a0f84243ec..0de2aee5ed 100644 --- a/client/webserver/site/src/localized_html/en-US/forms.tmpl +++ b/client/webserver/site/src/localized_html/en-US/forms.tmpl @@ -372,6 +372,78 @@
{{end}} +{{define "accelerateForm"}} +{{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+
+ Accelerate Order +
+
+
+ If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. The effective fee rate of your swap transactions will become the rate you select below. Select a rate that is enough to be included in the next block. Consult a block explorer to be sure. +
+
+ Effective swap tx fee rate: +
+
+ Current suggested fee rate: +
+
+
+
+ Increasing the effective fee rate to will cost +
+
+
+
+ + +
+
+ +
+
+
+
+
Your latest acceleration was only minutes ago! Are you sure you want to accelerate?
+
Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?
+
+ It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions. +
+
+ +
+
+
+
+
+
+
+ +
+
+
+ +
+
+ + + + + + + + +
+
+
+
+
+ Successfully submitted transaction: +
+
+{{end}} + {{define "waitingForWalletForm"}}
diff --git a/client/webserver/site/src/localized_html/en-US/markets.tmpl b/client/webserver/site/src/localized_html/en-US/markets.tmpl index c5a870f0d0..cdf8416b0d 100644 --- a/client/webserver/site/src/localized_html/en-US/markets.tmpl +++ b/client/webserver/site/src/localized_html/en-US/markets.tmpl @@ -389,6 +389,7 @@ + @@ -655,7 +656,9 @@
{{template "cancelOrderForm" .}}
- +
+ {{template "accelerateForm" .}} +
diff --git a/client/webserver/site/src/localized_html/en-US/order.tmpl b/client/webserver/site/src/localized_html/en-US/order.tmpl index 182bef25a2..320c75a3e3 100644 --- a/client/webserver/site/src/localized_html/en-US/order.tmpl +++ b/client/webserver/site/src/localized_html/en-US/order.tmpl @@ -84,12 +84,14 @@
Age
-
-
Accelerate
-
-
{{- /* END DATA CARDS */ -}} + {{- /* ACTIONS */ -}} +
Actions
+
+ +
+ {{- /* MATCHES */ -}}
Matches
@@ -223,79 +225,9 @@ {{template "cancelOrderForm" .}}
- {{$passwordIsCached := .UserInfo.PasswordIsCached}} -
-
- Accelerate Order -
-
-
- If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block. -
-
- Effective swap tx fee rate: -
-
- Current suggested fee rate: -
-
-
-
- Increasing the effective fee rate to will cost -
-
-
-
- - -
-
- -
-
-
-
-
Your latest acceleration was only minutes ago! Are you sure you want to accelerate?
-
Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?
-
- It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions. -
-
- -
-
-
-
-
-
-
-
- Successfully submitted transaction: -
-
+ {{template "accelerateForm" .}}
- -
-
- -
-
-
- -
-
- - - - - - - - -
-
-
{{template "bottom"}} {{end}} diff --git a/client/webserver/site/src/localized_html/pl-PL/forms.tmpl b/client/webserver/site/src/localized_html/pl-PL/forms.tmpl index a01db0cc58..028651df47 100644 --- a/client/webserver/site/src/localized_html/pl-PL/forms.tmpl +++ b/client/webserver/site/src/localized_html/pl-PL/forms.tmpl @@ -372,6 +372,78 @@
{{end}} +{{define "accelerateForm"}} +{{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+
+ Accelerate Order +
+
+
+ If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. The effective fee rate of your swap transactions will become the rate you select below. Select a rate that is enough to be included in the next block. Consult a block explorer to be sure. +
+
+ Effective swap tx fee rate: +
+
+ Current suggested fee rate: +
+
+
+
+ Increasing the effective fee rate to will cost +
+
+
+
+ + +
+
+ +
+
+
+
+
Your latest acceleration was only minutes ago! Are you sure you want to accelerate?
+
Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?
+
+ It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions. +
+
+ +
+
+
+
+
+
+
+ +
+
+
+ +
+
+ + + + + + + + +
+
+
+
+
+ Successfully submitted transaction: +
+
+{{end}} + {{define "waitingForWalletForm"}}
diff --git a/client/webserver/site/src/localized_html/pl-PL/markets.tmpl b/client/webserver/site/src/localized_html/pl-PL/markets.tmpl index b87aa9ba94..f6f5c2c4da 100644 --- a/client/webserver/site/src/localized_html/pl-PL/markets.tmpl +++ b/client/webserver/site/src/localized_html/pl-PL/markets.tmpl @@ -389,6 +389,7 @@ + @@ -655,7 +656,9 @@
{{template "cancelOrderForm" .}}
- +
+ {{template "accelerateForm" .}} +
diff --git a/client/webserver/site/src/localized_html/pl-PL/order.tmpl b/client/webserver/site/src/localized_html/pl-PL/order.tmpl index 66223206bc..55cb828878 100644 --- a/client/webserver/site/src/localized_html/pl-PL/order.tmpl +++ b/client/webserver/site/src/localized_html/pl-PL/order.tmpl @@ -84,12 +84,14 @@
Wiek
-
-
Accelerate
-
-
{{- /* END DATA CARDS */ -}} + {{- /* ACTIONS */ -}} +
Czynności
+
+ +
+ {{- /* MATCHES */ -}}
Spasowane zlecenia
@@ -223,79 +225,9 @@ {{template "cancelOrderForm" .}}
- {{$passwordIsCached := .UserInfo.PasswordIsCached}} -
-
- Accelerate Order -
-
-
- If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block. -
-
- Effective swap tx fee rate: -
-
- Current suggested fee rate: -
-
-
-
- Increasing the effective fee rate to will cost -
-
-
-
- - -
-
- -
-
-
-
-
Your latest acceleration was only minutes ago! Are you sure you want to accelerate?
-
Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?
-
- It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions. -
-
- -
-
-
-
-
-
-
-
- Successfully submitted transaction: -
-
+ {{template "accelerateForm" .}}
- -
-
- -
-
-
- -
-
- - - - - - - - -
-
-
{{template "bottom"}} {{end}} diff --git a/client/webserver/site/src/localized_html/pt-BR/forms.tmpl b/client/webserver/site/src/localized_html/pt-BR/forms.tmpl index eeba9574b1..a6fca23477 100644 --- a/client/webserver/site/src/localized_html/pt-BR/forms.tmpl +++ b/client/webserver/site/src/localized_html/pt-BR/forms.tmpl @@ -372,6 +372,78 @@
{{end}} +{{define "accelerateForm"}} +{{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+
+ Accelerate Order +
+
+
+ If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. The effective fee rate of your swap transactions will become the rate you select below. Select a rate that is enough to be included in the next block. Consult a block explorer to be sure. +
+
+ Effective swap tx fee rate: +
+
+ Current suggested fee rate: +
+
+
+
+ Increasing the effective fee rate to will cost +
+
+
+
+ + +
+
+ +
+
+
+
+
Your latest acceleration was only minutes ago! Are you sure you want to accelerate?
+
Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?
+
+ It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions. +
+
+ +
+
+
+
+
+
+
+ +
+
+
+ +
+
+ + + + + + + + +
+
+
+
+
+ Successfully submitted transaction: +
+
+{{end}} + {{define "waitingForWalletForm"}}
diff --git a/client/webserver/site/src/localized_html/pt-BR/markets.tmpl b/client/webserver/site/src/localized_html/pt-BR/markets.tmpl index 223a8c0458..cf3af2a5a0 100644 --- a/client/webserver/site/src/localized_html/pt-BR/markets.tmpl +++ b/client/webserver/site/src/localized_html/pt-BR/markets.tmpl @@ -389,6 +389,7 @@ + @@ -655,7 +656,9 @@
{{template "cancelOrderForm" .}}
- +
+ {{template "accelerateForm" .}} +
diff --git a/client/webserver/site/src/localized_html/pt-BR/order.tmpl b/client/webserver/site/src/localized_html/pt-BR/order.tmpl index aca7ca3d57..71bbf6b956 100644 --- a/client/webserver/site/src/localized_html/pt-BR/order.tmpl +++ b/client/webserver/site/src/localized_html/pt-BR/order.tmpl @@ -84,12 +84,14 @@
Idade
-
-
Accelerate
-
-
{{- /* END DATA CARDS */ -}} + {{- /* ACTIONS */ -}} +
Ações
+
+ +
+ {{- /* MATCHES */ -}}
Combinações
@@ -223,79 +225,9 @@ {{template "cancelOrderForm" .}}
- {{$passwordIsCached := .UserInfo.PasswordIsCached}} -
-
- Accelerate Order -
-
-
- If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block. -
-
- Effective swap tx fee rate: -
-
- Current suggested fee rate: -
-
-
-
- Increasing the effective fee rate to will cost -
-
-
-
- - -
-
- -
-
-
-
-
Your latest acceleration was only minutes ago! Are you sure you want to accelerate?
-
Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?
-
- It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions. -
-
- -
-
-
-
-
-
-
-
- Successfully submitted transaction: -
-
+ {{template "accelerateForm" .}}
- -
-
- -
-
-
- -
-
- - - - - - - - -
-
-
{{template "bottom"}} {{end}} diff --git a/client/webserver/site/src/localized_html/zh-CN/forms.tmpl b/client/webserver/site/src/localized_html/zh-CN/forms.tmpl index f53e0def64..5238e1fec8 100644 --- a/client/webserver/site/src/localized_html/zh-CN/forms.tmpl +++ b/client/webserver/site/src/localized_html/zh-CN/forms.tmpl @@ -372,6 +372,78 @@
{{end}} +{{define "accelerateForm"}} +{{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+
+ Accelerate Order +
+
+
+ If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. The effective fee rate of your swap transactions will become the rate you select below. Select a rate that is enough to be included in the next block. Consult a block explorer to be sure. +
+
+ Effective swap tx fee rate: +
+
+ Current suggested fee rate: +
+
+
+
+ Increasing the effective fee rate to will cost +
+
+
+
+ + +
+
+ +
+
+
+
+
Your latest acceleration was only minutes ago! Are you sure you want to accelerate?
+
Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?
+
+ It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions. +
+
+ +
+
+
+
+
+
+
+ +
+
+
+ +
+
+ + + + + + + + +
+
+
+
+
+ Successfully submitted transaction: +
+
+{{end}} + {{define "waitingForWalletForm"}}
diff --git a/client/webserver/site/src/localized_html/zh-CN/markets.tmpl b/client/webserver/site/src/localized_html/zh-CN/markets.tmpl index daa41c7d70..566d5fa15e 100644 --- a/client/webserver/site/src/localized_html/zh-CN/markets.tmpl +++ b/client/webserver/site/src/localized_html/zh-CN/markets.tmpl @@ -389,6 +389,7 @@ + @@ -655,7 +656,9 @@
{{template "cancelOrderForm" .}}
- +
+ {{template "accelerateForm" .}} +
diff --git a/client/webserver/site/src/localized_html/zh-CN/order.tmpl b/client/webserver/site/src/localized_html/zh-CN/order.tmpl index a7e71cbbda..2f6cd76569 100644 --- a/client/webserver/site/src/localized_html/zh-CN/order.tmpl +++ b/client/webserver/site/src/localized_html/zh-CN/order.tmpl @@ -84,12 +84,14 @@
年龄
-
-
Accelerate
-
-
{{- /* END DATA CARDS */ -}} + {{- /* ACTIONS */ -}} +
操作
+
+ +
+ {{- /* MATCHES */ -}}
组合
@@ -223,79 +225,9 @@ {{template "cancelOrderForm" .}}
- {{$passwordIsCached := .UserInfo.PasswordIsCached}} -
-
- Accelerate Order -
-
-
- If your swap transactions are stuck, you may attempt to accelerate them with an additional transaction. This is helpful when the the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. When you submit this form, a transaction will be created that sends the change from your own swap initiation transaction to yourself with a higher fee. With the new transaction, the average fee rate of all unconfirmed swap transactions for this order, plus the new acceleration transaction, will be the rate that you select below. This effective rate should be at least enough to be included in the next block. -
-
- Effective swap tx fee rate: -
-
- Current suggested fee rate: -
-
-
-
- Increasing the effective fee rate to will cost -
-
-
-
- - -
-
- -
-
-
-
-
Your latest acceleration was only minutes ago! Are you sure you want to accelerate?
-
Your oldest unconfirmed swap transaction was submitted only minutes ago! Are you sure you want to accelerate?
-
- It will cause no harm to your order, but you might be wasting money. Acceleration is only helpful if the fee rate on an existing unconfirmed transaction has become too low to be mined in the next block, but not if blocks are just being mined slowly. You can confirm this in the block explorer by closing this popup and clicking on your previous transactions. -
-
- -
-
-
-
-
-
-
-
- Successfully submitted transaction: -
-
+ {{template "accelerateForm" .}}
- -
-
- -
-
-
- -
-
- - - - - - - - -
-
- {{template "bottom"}} {{end}} From 44713453465f94c224c9706b2f3c78802fd5225f Mon Sep 17 00:00:00 2001 From: martonp Date: Tue, 17 May 2022 10:09:31 +0700 Subject: [PATCH 11/13] Cleanup client/btc based on chappjc review --- client/asset/btc/btc.go | 555 ++++++++++++++++++++--------------- client/asset/btc/btc_test.go | 386 ++++++++++++++++-------- client/asset/interface.go | 34 ++- client/core/core.go | 8 +- client/core/core_test.go | 6 +- client/core/trade.go | 13 +- client/core/wallet.go | 4 +- client/db/bolt/db.go | 2 +- 8 files changed, 612 insertions(+), 396 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 461f085d10..2fd30a329b 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -623,6 +623,7 @@ type ExchangeWalletAccelerator struct { // Check that wallets satisfy their supported interfaces. var _ asset.Wallet = (*baseWallet)(nil) var _ asset.Accelerator = (*ExchangeWalletAccelerator)(nil) +var _ asset.Accelerator = (*ExchangeWalletSPV)(nil) var _ asset.Rescanner = (*ExchangeWalletSPV)(nil) var _ asset.FeeRater = (*ExchangeWalletFullNode)(nil) var _ asset.LogFiler = (*ExchangeWalletSPV)(nil) @@ -1939,50 +1940,187 @@ func (btc *baseWallet) fundedTx(coins asset.Coins) (*wire.MsgTx, uint64, []outPo return baseTx, totalIn, pts, nil } -// signedAccelerationTx returns a signed transaction that sends funds to a -// change address controlled by this wallet. This new transaction will have -// a fee high enough to make the average fee of the unmined swapCoins and -// accelerationTxs to be newFeeRate. -func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (*wire.MsgTx, *output, uint64, error) { - makeError := func(err error) (*wire.MsgTx, *output, uint64, error) { - return nil, nil, 0, err +// lookupWalletTxOutput looks up the value of a transaction output that is +// spandable by this wallet, and creates an output. +func (btc *baseWallet) lookupWalletTxOutput(txHash *chainhash.Hash, vout uint32) (*output, error) { + getTxResult, err := btc.node.getWalletTransaction(txHash) + if err != nil { + return nil, err } - changeTxHash, changeVout, err := decodeCoinID(changeCoin) + tx, err := msgTxFromBytes(getTxResult.Hex) if err != nil { - return makeError(err) + return nil, err + } + if len(tx.TxOut) <= int(vout) { + return nil, fmt.Errorf("txId %s only has %d outputs. tried to access index %d", + txHash, len(tx.TxOut), vout) + } + + value := tx.TxOut[vout].Value + return newOutput(txHash, vout, uint64(value)), nil +} + +// getTransactions retrieves the transactions that created coins. The +// returned slice will be in the same order as the argument. +func (btc *baseWallet) getTransactions(coins []dex.Bytes) ([]*GetTransactionResult, error) { + txs := make([]*GetTransactionResult, 0, len(coins)) + if len(coins) == 0 { + return txs, nil + } + + for _, coinID := range coins { + txHash, _, err := decodeCoinID(coinID) + if err != nil { + return nil, err + } + getTxRes, err := btc.node.getWalletTransaction(txHash) + if err != nil { + return nil, err + } + txs = append(txs, getTxRes) + } + + return txs, nil +} + +func (btc *baseWallet) getTxFee(tx *wire.MsgTx) (uint64, error) { + var in, out uint64 + + for _, txOut := range tx.TxOut { + out += uint64(txOut.Value) + } + + for _, txIn := range tx.TxIn { + prevTx, err := btc.node.getWalletTransaction(&txIn.PreviousOutPoint.Hash) + if err != nil { + return 0, err + } + prevMsgTx, err := msgTxFromBytes(prevTx.Hex) + if err != nil { + return 0, err + } + if len(prevMsgTx.TxOut) <= int(txIn.PreviousOutPoint.Index) { + return 0, fmt.Errorf("tx %x references index %d output of %x, but it only has %d outputs", + tx.TxHash(), txIn.PreviousOutPoint.Index, prevMsgTx.TxHash(), len(prevMsgTx.TxOut)) + } + in += uint64(prevMsgTx.TxOut[int(txIn.PreviousOutPoint.Index)].Value) + } + + return in - out, nil +} + +// sizeAndFeesOfConfirmedTxs returns the total size in vBytes and the total +// fees spent by the unconfirmed transactions in txs. +func (btc *baseWallet) sizeAndFeesOfUnconfirmedTxs(txs []*GetTransactionResult) (size uint64, fees uint64, err error) { + for _, tx := range txs { + if tx.Confirmations > 0 { + continue + } + + msgTx, err := msgTxFromBytes(tx.Hex) + if err != nil { + return 0, 0, err + } + + fee, err := btc.getTxFee(msgTx) + if err != nil { + return 0, 0, err + } + + fees += fee + size += dexbtc.MsgTxVBytes(msgTx) } - err = btc.changeCanBeAccelerated(changeTxHash, changeVout, requiredForRemainingSwaps) + return size, fees, nil +} + +// additionalFeesRequired calculates the additional satoshis that need to be +// sent to miners in order to increase the average fee rate of unconfirmed +// transactions to newFeeRate. An error is returned if no additional fees +// are required. +func (btc *baseWallet) additionalFeesRequired(txs []*GetTransactionResult, newFeeRate uint64) (uint64, error) { + size, fees, err := btc.sizeAndFeesOfUnconfirmedTxs(txs) if err != nil { - return makeError(err) + return 0, err } - txs, err := btc.getTransactions(append(swapCoins, accelerationCoins...)) + if fees >= size*newFeeRate { + return 0, fmt.Errorf("extra fees are not needed. %d would be needed "+ + "for a fee rate of %d, but %d was already paid", + size*newFeeRate, newFeeRate, fees) + } + + return size*newFeeRate - fees, nil +} + +// changeCanBeAccelerated returns nil if the change can be accelerated, +// otherwise it returns an error containing the reason why it cannot. +func (btc *baseWallet) changeCanBeAccelerated(change *output, remainingSwaps bool) error { + lockedUtxos, err := btc.node.listLockUnspent() if err != nil { - return makeError(fmt.Errorf("failed to sort swap chain: %w", err)) + return err + } + + changeTxHash := change.pt.txHash.String() + for _, utxo := range lockedUtxos { + if utxo.TxID == changeTxHash && utxo.Vout == change.pt.vout { + if !remainingSwaps { + return errors.New("change locked by another order") + } else { + // change is locked by this order + return nil + } + } } - changeOutput, err := btc.lookupOutput(changeTxHash, changeVout) + utxos, err := btc.node.listUnspent() if err != nil { - return makeError(err) + return err + } + for _, utxo := range utxos { + if utxo.TxID == changeTxHash && utxo.Vout == change.pt.vout { + return nil + } + } + + return errors.New("change already spent") +} + +// signedAccelerationTx returns a signed transaction that sends funds to a +// change address controlled by this wallet. This new transaction will have +// a fee high enough to make the average fee of the unmined previousTxs to +// the newFeeRate. orderChange latest change in the order, and must be spent +// by this new transaction in order to accelerate the order. +// requiredForRemainingSwaps is the amount of funds that are still required +// to complete the order, so the change of the acceleration transaction must +// contain at least that amount. +func (btc *baseWallet) signedAccelerationTx(previousTxs []*GetTransactionResult, orderChange *output, requiredForRemainingSwaps, newFeeRate uint64) (*wire.MsgTx, *output, uint64, error) { + makeError := func(err error) (*wire.MsgTx, *output, uint64, error) { + return nil, nil, 0, err } - additionalFeesRequired, err := btc.additionalFeesRequired(txs, newFeeRate) + err := btc.changeCanBeAccelerated(orderChange, requiredForRemainingSwaps > 0) if err != nil { return makeError(err) } - if additionalFeesRequired <= 0 { - return makeError(fmt.Errorf("no additional fees are required to move the fee rate to %v", newFeeRate)) + additionalFeesRequired, err := btc.additionalFeesRequired(previousTxs, newFeeRate) + if err != nil { + return makeError(err) } + // Figure out how much funds we need to increase the fee to the requested + // amount. txSize := uint64(dexbtc.MinimumTxOverhead) + // Add the size of using the order change as an input if btc.segwit { txSize += dexbtc.RedeemP2WPKHInputTotalSize } else { txSize += dexbtc.RedeemP2PKHInputSize } + // We need an output if funds are still required for additional swaps in + // the order. if requiredForRemainingSwaps > 0 { if btc.segwit { txSize += dexbtc.P2WPKHOutputSize @@ -1990,11 +2128,10 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B txSize += dexbtc.P2PKHOutputSize } } - fundsRequired := additionalFeesRequired + requiredForRemainingSwaps + txSize*newFeeRate var additionalInputs asset.Coins - if fundsRequired > changeOutput.value { + if fundsRequired > orderChange.value { // If change not enough, need to use other UTXOs. utxos, _, _, err := btc.spendableUTXOs(1) if err != nil { @@ -2004,7 +2141,7 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B _, _, additionalInputs, _, _, _, err = fund(utxos, func(inputSize, inputsVal uint64) bool { txSize := dexbtc.MinimumTxOverhead + inputSize - // input is the change input that we must use + // add the order change as an input if btc.segwit { txSize += dexbtc.RedeemP2WPKHInputTotalSize } else { @@ -2020,14 +2157,14 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B } totalFees := additionalFeesRequired + txSize*newFeeRate - return totalFees+requiredForRemainingSwaps <= inputsVal+changeOutput.value + return totalFees+requiredForRemainingSwaps <= inputsVal+orderChange.value }) if err != nil { return makeError(fmt.Errorf("failed to fund acceleration tx: %w", err)) } } - baseTx, totalIn, _, err := btc.fundedTx(append(additionalInputs, changeOutput)) + baseTx, totalIn, _, err := btc.fundedTx(append(additionalInputs, orderChange)) if err != nil { return makeError(err) } @@ -2049,9 +2186,12 @@ func (btc *baseWallet) signedAccelerationTx(swapCoins, accelerationCoins []dex.B // chain of swap transactions and previous accelerations. It broadcasts a new // transaction with a fee high enough so that the average fee of all the // unconfirmed transactions in the chain and the new transaction will have -// an average fee rate of newFeeRate. requiredForRemainingSwaps is passed -// in to ensure that the new change coin will have enough funds to initiate -// the additional swaps that will be required to complete the order. +// an average fee rate of newFeeRate. The changeCoin argument is the latest +// chhange in the order. It must be the input in the acceleration transaction +// in order for the order to be accelerated. requiredForRemainingSwaps is the +// amount of funds required to complete the rest of the swaps in the order. +// The change output of the acceleration transaction will have at least +// this amount. // // The returned change coin may be nil, and should be checked before use. func (btc *ExchangeWalletAccelerator) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { @@ -2062,9 +2202,12 @@ func (btc *ExchangeWalletAccelerator) AccelerateOrder(swapCoins, accelerationCoi // chain of swap transactions and previous accelerations. It broadcasts a new // transaction with a fee high enough so that the average fee of all the // unconfirmed transactions in the chain and the new transaction will have -// an average fee rate of newFeeRate. requiredForRemainingSwaps is passed -// in to ensure that the new change coin will have enough funds to initiate -// the additional swaps that will be required to complete the order. +// an average fee rate of newFeeRate. The changeCoin argument is the latest +// chhange in the order. It must be the input in the acceleration transaction +// in order for the order to be accelerated. requiredForRemainingSwaps is the +// amount of funds required to complete the rest of the swaps in the order. +// The change output of the acceleration transaction will have at least +// this amount. // // The returned change coin may be nil, and should be checked before use. func (btc *ExchangeWalletSPV) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { @@ -2079,12 +2222,19 @@ func accelerateOrder(btc *baseWallet, swapCoins, accelerationCoins []dex.Bytes, if err != nil { return nil, "", err } - - signedTx, newChange, _, err := btc.signedAccelerationTx(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) + changeOutput, err := btc.lookupWalletTxOutput(changeTxHash, changeVout) + if err != nil { + return nil, "", err + } + previousTxs, err := btc.getTransactions(append(swapCoins, accelerationCoins...)) + if err != nil { + return nil, "", err + } + signedTx, newChange, _, err := + btc.signedAccelerationTx(previousTxs, changeOutput, requiredForRemainingSwaps, newFeeRate) if err != nil { return nil, "", err } - err = btc.broadcastTx(signedTx) if err != nil { return nil, "", err @@ -2097,10 +2247,11 @@ func accelerateOrder(btc *baseWallet, swapCoins, accelerationCoins []dex.Bytes, return nil, signedTx.TxHash().String(), nil } - // Checking required for remaining swaps > 0 because this ensures if - // the previous change was locked, this one will also be locked. If + // Add the new change to the cache if needed. We check if + // required for remaining swaps > 0 because this ensures if the + // previous change was locked, this one will also be locked. If // requiredForRemainingSwaps = 0, but the change was locked, - // signedAccelerationTx would have returned an error since this means + // changeCanBeAccelerated would have returned an error since this means // that the change was locked by another order. if requiredForRemainingSwaps > 0 { err = btc.node.lockUnspent(false, []*output{newChange}) @@ -2143,7 +2294,22 @@ func (btc *ExchangeWalletSPV) AccelerationEstimate(swapCoins, accelerationCoins func accelerationEstimate(btc *baseWallet, swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) { btc.fundingMtx.RLock() defer btc.fundingMtx.RUnlock() - _, _, fee, err := btc.signedAccelerationTx(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) + + previousTxs, err := btc.getTransactions(append(swapCoins, accelerationCoins...)) + if err != nil { + return 0, fmt.Errorf("failed to get transactions: %w", err) + } + + changeTxHash, changeVout, err := decodeCoinID(changeCoin) + if err != nil { + return 0, err + } + changeOutput, err := btc.lookupWalletTxOutput(changeTxHash, changeVout) + if err != nil { + return 0, err + } + + _, _, fee, err := btc.signedAccelerationTx(previousTxs, changeOutput, requiredForRemainingSwaps, newFeeRate) if err != nil { return 0, err } @@ -2151,49 +2317,53 @@ func accelerationEstimate(btc *baseWallet, swapCoins, accelerationCoins []dex.By return fee, nil } -// tooEarlyToAccelerate returns true if minTimeBeforeAcceleration has not passed -// since either the earliest unconfirmed transaction in the chain, or the -// latest acceleration transaction. It also returns the time that passed since -// either of these events, and whether or not the event was an acceleration -// transaction. -func tooEarlyToAccelerate(txs []*GetTransactionResult, accelerationCoins []dex.Bytes) (bool, bool, uint64, error) { - accelerationTxs := make(map[string]bool, len(accelerationCoins)) - for _, accelerationCoin := range accelerationCoins { - txHash, _, err := decodeCoinID(accelerationCoin) - if err != nil { - return false, false, 0, err - } - accelerationTxs[txHash.String()] = true +// tooEarlyToAccelerate returns an asset.EarlyAcceleration if +// minTimeBeforeAcceleration has not passed since either the earliest +// unconfirmed swap transaction, or the latest acceleration transaction. +func tooEarlyToAccelerate(swapTxs []*GetTransactionResult, accelerationTxs []*GetTransactionResult) (*asset.EarlyAcceleration, error) { + accelerationTxLookup := make(map[string]bool, len(accelerationTxs)) + for _, accelerationCoin := range accelerationTxs { + accelerationTxLookup[accelerationCoin.TxID] = true } - var latestAcceleration, earliestUnconfirmed uint64 - for _, tx := range txs { + var latestAcceleration, earliestUnconfirmed uint64 = 0, math.MaxUint64 + for _, tx := range swapTxs { if tx.Confirmations > 0 { continue } - if accelerationTxs[tx.TxID] && tx.Time > latestAcceleration { - latestAcceleration = tx.Time - continue - } - if earliestUnconfirmed == 0 || tx.Time < earliestUnconfirmed { + if tx.Time < earliestUnconfirmed { earliestUnconfirmed = tx.Time } } - if latestAcceleration == 0 && earliestUnconfirmed == 0 { - return false, false, 0, fmt.Errorf("no need to accelerate because all tx are confirmed") + for _, tx := range accelerationTxs { + if tx.Confirmations > 0 { + continue + } + if tx.Time > latestAcceleration { + latestAcceleration = tx.Time + } } var actionTime uint64 var wasAccelerated bool - if latestAcceleration != 0 { - wasAccelerated = true - actionTime = latestAcceleration - } else { + if latestAcceleration == 0 && earliestUnconfirmed == math.MaxUint64 { + return nil, fmt.Errorf("no need to accelerate because all tx are confirmed") + } else if earliestUnconfirmed > latestAcceleration && earliestUnconfirmed < math.MaxUint64 { actionTime = earliestUnconfirmed + } else { + actionTime = latestAcceleration + wasAccelerated = true } currentTime := uint64(time.Now().Unix()) - return actionTime+minTimeBeforeAcceleration > currentTime, wasAccelerated, currentTime - actionTime, nil + if actionTime+minTimeBeforeAcceleration > currentTime { + return &asset.EarlyAcceleration{ + TimePast: currentTime - actionTime, + WasAccelerated: wasAccelerated, + }, nil + } + + return nil, nil } // PreAccelerate returns the current average fee rate of the unmined swap @@ -2204,7 +2374,7 @@ func tooEarlyToAccelerate(txs []*GetTransactionResult, accelerationCoins []dex.B // the user a good amount of flexibility in determining the post acceleration // effective fee rate, but still not allowing them to pick something // outrageously high. -func (btc *ExchangeWalletAccelerator) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { +func (btc *ExchangeWalletAccelerator) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, *asset.XYRange, *asset.EarlyAcceleration, error) { return preAccelerate(btc.baseWallet, swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion) } @@ -2216,109 +2386,109 @@ func (btc *ExchangeWalletAccelerator) PreAccelerate(swapCoins, accelerationCoins // the user a good amount of flexibility in determining the post acceleration // effective fee rate, but still not allowing them to pick something // outrageously high. -func (btc *ExchangeWalletSPV) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { +func (btc *ExchangeWalletSPV) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, *asset.XYRange, *asset.EarlyAcceleration, error) { return preAccelerate(btc.baseWallet, swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion) } -func preAccelerate(btc *baseWallet, swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { - makeError := func(err error) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { - return 0, asset.XYRange{}, nil, err +// maxAccelerationRate returns the max rate to which an order can be +// accelerated, if the max rate is less than rateNeeded. If the max rate is +// greater than rateNeeded, rateNeeded is returned. +func (btc *baseWallet) maxAccelerationRate(changeVal, feesAlreadyPaid, orderTxVBytes, requiredForRemainingSwaps, rateNeeded uint64) (uint64, error) { + var txSize, witnessSize, additionalUtxosVal uint64 + + // First, add all the elements that will definitely be part of the + // acceleration transaction, without any additional inputs. + txSize += dexbtc.MinimumTxOverhead + if btc.segwit { + txSize += dexbtc.RedeemP2WPKHInputSize + witnessSize += dexbtc.RedeemP2WPKHInputWitnessWeight + } else { + txSize += dexbtc.RedeemP2PKHInputSize + } + if requiredForRemainingSwaps > 0 { + if btc.segwit { + txSize += dexbtc.P2WPKHOutputSize + } else { + txSize += dexbtc.P2PKHOutputSize + } } - changeTxHash, changeVout, err := decodeCoinID(changeCoin) - if err != nil { - return makeError(err) + calcFeeRate := func() uint64 { + accelerationTxVBytes := txSize + (witnessSize+3)/4 + totalValue := changeVal + feesAlreadyPaid + additionalUtxosVal + if totalValue < requiredForRemainingSwaps { + return 0 + } + totalValue -= requiredForRemainingSwaps + totalSize := accelerationTxVBytes + orderTxVBytes + return totalValue / totalSize } - err = btc.changeCanBeAccelerated(changeTxHash, changeVout, requiredForRemainingSwaps) - if err != nil { - return makeError(err) + if calcFeeRate() >= rateNeeded { + return rateNeeded, nil } - txs, err := btc.getTransactions(append(swapCoins, accelerationCoins...)) + // If necessary, use as many additional utxos as needed + btc.fundingMtx.RLock() + utxos, _, _, err := btc.spendableUTXOs(1) + btc.fundingMtx.RUnlock() if err != nil { - return makeError(fmt.Errorf("failed to sort swap chain: %w", err)) + return 0, err } - var swapTxsSize, feesAlreadyPaid uint64 - for _, tx := range txs { - if tx.Confirmations > 0 { + for _, utxo := range utxos { + if utxo.input.NonStandardScript { continue } - feesAlreadyPaid += toSatoshi(math.Abs(tx.Fee)) - msgTx, err := msgTxFromBytes(tx.Hex) - if err != nil { - return makeError(err) + txSize += dexbtc.TxInOverhead + + uint64(wire.VarIntSerializeSize(uint64(utxo.input.SigScriptSize))) + + uint64(utxo.input.SigScriptSize) + witnessSize += uint64(utxo.input.WitnessSize) + additionalUtxosVal += utxo.amount + if calcFeeRate() >= rateNeeded { + return rateNeeded, nil } - swapTxsSize += dexbtc.MsgTxVBytes(msgTx) } - // Is it safe to assume that transactions will all have some fee? - if feesAlreadyPaid == 0 { - return makeError(fmt.Errorf("all transactions are already confirmed, no need to accelerate")) + return calcFeeRate(), nil +} + +func preAccelerate(btc *baseWallet, swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, *asset.XYRange, *asset.EarlyAcceleration, error) { + makeError := func(err error) (uint64, *asset.XYRange, *asset.EarlyAcceleration, error) { + return 0, &asset.XYRange{}, nil, err } - var earlyAcceleration *asset.EarlyAcceleration - tooEarly, isAcceleration, timePast, err := tooEarlyToAccelerate(txs, accelerationCoins) + changeTxHash, changeVout, err := decodeCoinID(changeCoin) if err != nil { return makeError(err) } - if tooEarly { - earlyAcceleration = &asset.EarlyAcceleration{ - TimePast: timePast, - WasAcclerated: isAcceleration, - } - } - - btc.fundingMtx.RLock() - utxos, _, utxosVal, err := btc.spendableUTXOs(1) - btc.fundingMtx.RUnlock() + changeOutput, err := btc.lookupWalletTxOutput(changeTxHash, changeVout) if err != nil { return makeError(err) } - // This is to avoid having too many inputs, and causing an error during signing - if len(utxos) > 500 { - utxos = utxos[len(utxos)-500:] - utxosVal = 0 - for _, utxo := range utxos { - utxosVal += utxo.amount - } - } - - var coins asset.Coins - for _, utxo := range utxos { - coins = append(coins, newOutput(utxo.txHash, utxo.vout, utxo.amount)) - } - - changeOutput, err := btc.lookupOutput(changeTxHash, changeVout) + err = btc.changeCanBeAccelerated(changeOutput, requiredForRemainingSwaps > 0) if err != nil { return makeError(err) } - tx, _, _, err := btc.fundedTx(append(coins, changeOutput)) + txs, err := btc.getTransactions(append(swapCoins, accelerationCoins...)) if err != nil { - return makeError(err) + return makeError(fmt.Errorf("failed to get transactions: %w", err)) } - tx, err = btc.node.signTx(tx) + + existingTxSize, feesAlreadyPaid, err := btc.sizeAndFeesOfUnconfirmedTxs(txs) if err != nil { return makeError(err) } - - newChangeTxSize := dexbtc.MsgTxVBytes(tx) - if requiredForRemainingSwaps > 0 { - if btc.segwit { - newChangeTxSize += dexbtc.P2WPKHOutputSize - } else { - newChangeTxSize += dexbtc.P2PKHOutputSize - } + // Is it safe to assume that transactions will all have some fee? + if feesAlreadyPaid == 0 { + return makeError(fmt.Errorf("all transactions are already confirmed, no need to accelerate")) } - maxRate := (changeOutput.value + feesAlreadyPaid + utxosVal - requiredForRemainingSwaps) / (newChangeTxSize + swapTxsSize) - currentEffectiveRate := feesAlreadyPaid / swapTxsSize - - if maxRate <= currentEffectiveRate { - return makeError(fmt.Errorf("cannot accelerate, max rate %v <= current rate %v", maxRate, currentEffectiveRate)) + earlyAcceleration, err := tooEarlyToAccelerate(txs[:len(swapCoins)], txs[len(swapCoins):]) + if err != nil { + return makeError(err) } // The suggested range will be the min and max of the slider that is @@ -2331,13 +2501,26 @@ func preAccelerate(btc *baseWallet, swapCoins, accelerationCoins []dex.Bytes, ch // than the current effective rate, they will still have a comformtable // buffer above the prevailing network rate. const scalingFactor = 5 + currentEffectiveRate := feesAlreadyPaid / existingTxSize maxSuggestion := currentEffectiveRate * scalingFactor if feeSuggestion > currentEffectiveRate { maxSuggestion = feeSuggestion * scalingFactor } + + // We must make sure that the wallet can fund an acceleration at least + // the max suggestion, and if not, lower the max suggestion to the max + // rate that the wallet can fund. + maxRate, err := btc.maxAccelerationRate(changeOutput.value, feesAlreadyPaid, existingTxSize, requiredForRemainingSwaps, maxSuggestion) + if err != nil { + return makeError(err) + } + if maxRate <= currentEffectiveRate { + return makeError(fmt.Errorf("cannot accelerate, max rate %v <= current rate %v", maxRate, currentEffectiveRate)) + } if maxRate < maxSuggestion { maxSuggestion = maxRate } + suggestedRange := asset.XYRange{ Start: asset.XYRangePoint{ Label: "Min", @@ -2353,117 +2536,7 @@ func preAccelerate(btc *baseWallet, swapCoins, accelerationCoins []dex.Bytes, ch YUnit: btc.walletInfo.UnitInfo.AtomicUnit + "/" + btc.sizeUnit(), } - return currentEffectiveRate, suggestedRange, earlyAcceleration, nil -} - -// lookupOutput looks up the value of a transaction output and creates an -// output. -func (btc *baseWallet) lookupOutput(txHash *chainhash.Hash, vout uint32) (*output, error) { - getTxResult, err := btc.node.getWalletTransaction(txHash) - if err != nil { - return nil, err - } - tx, err := msgTxFromBytes(getTxResult.Hex) - if err != nil { - return nil, err - } - if len(tx.TxOut) <= int(vout) { - return nil, fmt.Errorf("txId %s only has %d outputs. tried to access index %d", - txHash, len(tx.TxOut), vout) - } - - value := tx.TxOut[vout].Value - return newOutput(txHash, vout, uint64(value)), nil -} - -// changeCanBeAccelerated returns whether or not the change output can be -// accelerated. -func (btc *baseWallet) changeCanBeAccelerated(changeTxHash *chainhash.Hash, changeVout uint32, requiredForRemainingSwaps uint64) error { - lockedUtxos, err := btc.node.listLockUnspent() - if err != nil { - return err - } - - var changeIsLocked bool - for _, utxo := range lockedUtxos { - if utxo.TxID == changeTxHash.String() && utxo.Vout == changeVout { - changeIsLocked = true - break - } - } - - if changeIsLocked && requiredForRemainingSwaps == 0 { - return errors.New("change cannot be accelerated because it is locked by another order") - } - - if !changeIsLocked { - utxos, err := btc.node.listUnspent() - if err != nil { - return err - } - - var changeIsUnspent bool - for _, utxo := range utxos { - if utxo.TxID == changeTxHash.String() && utxo.Vout == changeVout { - changeIsUnspent = true - break - } - } - - if !changeIsUnspent { - return errors.New("change cannot be accelerated because it has already been spent") - } - } - - return nil -} - -// getTransactions retrieves the transactions that created coins. -func (btc *baseWallet) getTransactions(coins []dex.Bytes) ([]*GetTransactionResult, error) { - txChain := make([]*GetTransactionResult, 0, len(coins)) - if len(coins) == 0 { - return txChain, nil - } - - for _, coinID := range coins { - txHash, _, err := decodeCoinID(coinID) - if err != nil { - return nil, err - } - getTxRes, err := btc.node.getWalletTransaction(txHash) - if err != nil { - return nil, err - } - txChain = append(txChain, getTxRes) - } - - return txChain, nil -} - -// additionalFeesRequired calculates the additional satoshis that need to be -// sent to miners in order to increase the average fee rate of unconfirmed -// transactions to newFeeRate. -func (btc *baseWallet) additionalFeesRequired(txs []*GetTransactionResult, newFeeRate uint64) (uint64, error) { - var totalTxSize, feesAlreadyPaid uint64 - for _, tx := range txs { - if tx.Confirmations > 0 { - continue - } - msgTx, err := msgTxFromBytes(tx.Hex) - if err != nil { - return 0, err - } - totalTxSize += dexbtc.MsgTxVBytes(msgTx) - feesAlreadyPaid += toSatoshi(math.Abs(tx.Fee)) - } - - if feesAlreadyPaid >= totalTxSize*newFeeRate { - return 0, fmt.Errorf("extra fees are not needed. %d would be needed "+ - "for a fee rate of %d, but %d was already paid", - totalTxSize*newFeeRate, newFeeRate, feesAlreadyPaid) - } - - return totalTxSize*newFeeRate - feesAlreadyPaid, nil + return currentEffectiveRate, &suggestedRange, earlyAcceleration, nil } // Swap sends the swaps in a single transaction and prepares the receipts. The diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index 895e84b124..bc00a1da6c 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "errors" "fmt" "math/rand" "os" @@ -3190,7 +3191,6 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { TxID: txs[i].TxHash().String(), Hex: serializedTxs[i], BlockHash: blockHash, - Fee: fees[i], Confirmations: confs[i]} if withinTimeLimit { @@ -3400,20 +3400,21 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { return uint64(txSize) } - changeAmount := int64(21350) + var changeAmount int64 = 21350 + var newFeeRate uint64 = 50 fees := []float64{0.00002, 0.000005, 0.00001, 0.00001} confs := []uint64{0, 0, 0, 0} _, _, _, txs := getAccelerationParams(changeAmount, false, false, fees, node) - expectedFees := (sumTxSizes(txs, confs)+calculateChangeTxSize(false, segwit, 1))*50 - sumFees(fees, confs) + expectedFees := (sumTxSizes(txs, confs)+calculateChangeTxSize(false, segwit, 1))*newFeeRate - sumFees(fees, confs) _, _, _, txs = getAccelerationParams(changeAmount, true, false, fees, node) - expectedFeesWithChange := (sumTxSizes(txs, confs)+calculateChangeTxSize(true, segwit, 1))*50 - sumFees(fees, confs) + expectedFeesWithChange := (sumTxSizes(txs, confs)+calculateChangeTxSize(true, segwit, 1))*newFeeRate - sumFees(fees, confs) // See dexbtc.IsDust for the source of this dustCoverage voodoo. var dustCoverage uint64 if segwit { - dustCoverage = (dexbtc.P2WPKHOutputSize + 41 + (107 / 4)) * 3 * 50 + dustCoverage = (dexbtc.P2WPKHOutputSize + 41 + (107 / 4)) * 3 * newFeeRate } else { - dustCoverage = (dexbtc.P2PKHOutputSize + 41 + 107) * 3 * 50 + dustCoverage = (dexbtc.P2PKHOutputSize + 41 + 107) * 3 * newFeeRate } type utxo struct { @@ -3463,7 +3464,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { changeAmount: int64(expectedFees), fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, confs: confs, - newFeeRate: 50, + newFeeRate: newFeeRate, suggestedFeeRate: 30, }, { @@ -3471,7 +3472,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { changeAmount: int64(expectedFees), fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, confs: confs, - newFeeRate: 50, + newFeeRate: newFeeRate, addPreviousAcceleration: true, }, { @@ -3479,7 +3480,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { changeAmount: int64(expectedFees), fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, confs: []uint64{0, 0, 0, 0}, - newFeeRate: 50, + newFeeRate: newFeeRate, scrambleSwapCoins: true, }, { @@ -3487,24 +3488,24 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { changeAmount: int64(expectedFees - 1), fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, confs: []uint64{0, 0, 0, 0}, - newFeeRate: 50, + newFeeRate: newFeeRate, expectAccelerationEstimateErr: true, expectAccelerateOrderErr: true, }, { - name: "add non dust amount to change", + name: "add greater than dust amount to change", changeAmount: int64(expectedFeesWithChange + dustCoverage), fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, confs: []uint64{0, 0, 0, 0}, - newFeeRate: 50, + newFeeRate: newFeeRate, expectChange: true, }, { - name: "add less than non dust amount to change", + name: "add dust amount to change", changeAmount: int64(expectedFeesWithChange + dustCoverage - 1), fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, confs: []uint64{0, 0, 0, 0}, - newFeeRate: 50, + newFeeRate: newFeeRate, expectChange: false, }, { @@ -3512,7 +3513,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { changeAmount: int64(expectedFeesWithChange + dustCoverage), fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, confs: []uint64{1, 1, 0, 0}, - newFeeRate: 50, + newFeeRate: newFeeRate, expectChange: true, }, { @@ -3520,7 +3521,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { changeAmount: int64(expectedFees - 1), fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, confs: []uint64{0, 0, 0, 0}, - newFeeRate: 50, + newFeeRate: newFeeRate, expectAccelerationEstimateErr: true, expectAccelerateOrderErr: true, }, @@ -3529,7 +3530,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { changeAmount: int64(expectedFees - 1), fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, confs: []uint64{0, 0, 0, 0}, - newFeeRate: 50, + newFeeRate: newFeeRate, expectAccelerationEstimateErr: true, expectAccelerateOrderErr: true, utxos: []utxo{{ @@ -3537,25 +3538,12 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { amount: 5e9, }}, }, - { - name: "not enough with 1-conf utxo in wallet", - changeAmount: int64(expectedFees - 1), - fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, - confs: []uint64{0, 0, 0, 0}, - newFeeRate: 50, - numUtxosUsed: 1, - expectChange: true, - utxos: []utxo{{ - confs: 1, - amount: 2e6, - }}, - }, { name: "enough with 1-conf utxo in wallet", changeAmount: int64(expectedFees - 1), fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, confs: []uint64{0, 0, 0, 0}, - newFeeRate: 50, + newFeeRate: newFeeRate, numUtxosUsed: 1, expectChange: true, utxos: []utxo{{ @@ -3568,7 +3556,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { changeAmount: int64(expectedFees - 1), fees: fees, confs: []uint64{0, 0, 0, 0}, - newFeeRate: 50, + newFeeRate: newFeeRate, numUtxosUsed: 1, expectAccelerationEstimateErr: true, expectAccelerateOrderErr: true, @@ -3584,7 +3572,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { changeAmount: int64(expectedFees - 1), fees: fees, confs: []uint64{0, 0, 0, 0}, - newFeeRate: 50, + newFeeRate: newFeeRate, numUtxosUsed: 2, expectChange: true, expectChangeLocked: true, @@ -3601,11 +3589,11 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { }, }, { - name: "locked change, required for remaining > 0", + name: "locked change, required for remaining >0", changeAmount: int64(expectedFees - 1), fees: fees, confs: []uint64{0, 0, 0, 0}, - newFeeRate: 50, + newFeeRate: newFeeRate, numUtxosUsed: 2, expectChange: true, expectChangeLocked: true, @@ -3627,7 +3615,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { changeAmount: int64(expectedFees - 1), fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, confs: []uint64{0, 0, 0, 0}, - newFeeRate: 50, + newFeeRate: newFeeRate, numUtxosUsed: 2, expectChange: true, expectAccelerationEstimateErr: true, @@ -3652,7 +3640,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { changeAmount: int64(expectedFees), fees: []float64{0.00002, 0.000005, 0.00001, 0.00001}, confs: confs, - newFeeRate: 50, + newFeeRate: newFeeRate, suggestedFeeRate: 30, }, } @@ -3811,7 +3799,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { t.Fatalf("%s: expected current rate %v != actual %v", test.name, expectedRate, currentRate) } - totalFeePossible := totalFee + totalFeePossible := totalFee + uint64(test.changeAmount) - test.requiredForRemainingSwaps var numConfirmedUTXO int for _, utxo := range test.utxos { if utxo.confs > 0 { @@ -3819,12 +3807,7 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { numConfirmedUTXO++ } } - totalSize += calculateChangeTxSize(test.expectChange, segwit, 1+numConfirmedUTXO) - - totalFeePossible += uint64(test.changeAmount) - totalFeePossible -= test.requiredForRemainingSwaps - maxRatePossible := totalFeePossible / totalSize expectedRangeHigh := expectedRate * 5 @@ -3857,102 +3840,258 @@ func testAccelerateOrder(t *testing.T, segwit bool, walletType string) { } } -func TestTooEarlyToAccelerate(t *testing.T) { +func TestGetTxFee(t *testing.T) { + runRubric(t, testGetTxFee) +} + +func testGetTxFee(t *testing.T, segwit bool, walletType string) { + w, node, shutdown, err := tNewWallet(segwit, walletType) + defer shutdown() + if err != nil { + t.Fatal(err) + } + + inputTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{ + { + Value: 2e6, + }, + { + Value: 3e6, + }, + }, + } + txBytes, err := serializeMsgTx(inputTx) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + node.getTransactionMap = map[string]*GetTransactionResult{ + "any": &GetTransactionResult{ + Hex: txBytes, + }, + } + tests := []struct { - name string - confirmations []uint64 - isAcceleration []bool - secondsBeforeNow []uint64 - expectTooEarly bool - expectTimePast uint64 - expectWasAcceleration bool - expectError bool + name string + tx *wire.MsgTx + expectErr bool + expectedFee uint64 + getTxErr bool }{ { - name: "all confirmed", - confirmations: []uint64{2, 2, 1, 1}, - isAcceleration: []bool{false, true, false, true}, - secondsBeforeNow: []uint64{ - minTimeBeforeAcceleration + 1000, - minTimeBeforeAcceleration + 800, - minTimeBeforeAcceleration + 500, - minTimeBeforeAcceleration + 300, + name: "ok", + tx: &wire.MsgTx{ + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: inputTx.TxHash(), + Index: 0, + }}, + {PreviousOutPoint: wire.OutPoint{ + Hash: inputTx.TxHash(), + Index: 1, + }}, + }, + TxOut: []*wire.TxOut{{ + Value: 4e6, + }}, }, - expectError: true, + expectedFee: 1e6, + }, + { + name: "get transaction error", + tx: &wire.MsgTx{ + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: inputTx.TxHash(), + Index: 0, + }}, + {PreviousOutPoint: wire.OutPoint{ + Hash: inputTx.TxHash(), + Index: 1, + }}, + }, + TxOut: []*wire.TxOut{{ + Value: 4e6, + }}, + }, + getTxErr: true, + expectErr: true, }, { - name: "no accelerations, not too early", - confirmations: []uint64{2, 0, 0, 2}, - isAcceleration: []bool{false, false, false, false}, - secondsBeforeNow: []uint64{ - minTimeBeforeAcceleration + 1000, - minTimeBeforeAcceleration + 500, - minTimeBeforeAcceleration + 300, - minTimeBeforeAcceleration + 800, + name: "invalid prev output index error", + tx: &wire.MsgTx{ + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: inputTx.TxHash(), + Index: 0, + }}, + {PreviousOutPoint: wire.OutPoint{ + Hash: inputTx.TxHash(), + Index: 2, + }}, + }, + TxOut: []*wire.TxOut{{ + Value: 4e6, + }}, }, - expectTooEarly: false, - expectTimePast: minTimeBeforeAcceleration + 500, + expectErr: true, + }, + } + + for _, test := range tests { + node.getTransactionErr = nil + if test.getTxErr { + node.getTransactionErr = errors.New("") + } + + fee, err := w.getTxFee(test.tx) + if test.expectErr { + if err == nil { + t.Fatalf("%s: expected error but did not get", test.name) + } + continue + } + if err != nil { + t.Fatalf("%s: unexpected error: %v", test.name, err) + } + if fee != test.expectedFee { + t.Fatalf("%s: expected fee %d, but got %d", test.name, test.expectedFee, fee) + } + } +} + +func TestTooEarlyToAccelerate(t *testing.T) { + type tx struct { + secondsBeforeNow uint64 + confirmations uint64 + } + tests := []struct { + name string + accelerations []tx + swaps []tx + expectedReturn *asset.EarlyAcceleration + expectError bool + }{ + { + name: "all confirmed", + accelerations: []tx{ + {secondsBeforeNow: minTimeBeforeAcceleration + 800, + confirmations: 2}, + {secondsBeforeNow: minTimeBeforeAcceleration + 300, + confirmations: 1}}, + swaps: []tx{ + {secondsBeforeNow: minTimeBeforeAcceleration + 1000, + confirmations: 2}, + {secondsBeforeNow: minTimeBeforeAcceleration + 500, + confirmations: 1}}, + expectError: true, + }, + { + name: "no accelerations, not too early", + accelerations: []tx{}, + swaps: []tx{ + {secondsBeforeNow: minTimeBeforeAcceleration + 1000, + confirmations: 2}, + {secondsBeforeNow: minTimeBeforeAcceleration + 500, + confirmations: 0}, + {secondsBeforeNow: minTimeBeforeAcceleration + 300, + confirmations: 0}, + {secondsBeforeNow: minTimeBeforeAcceleration + 800, + confirmations: 2}}, + }, + { + name: "acceleration after unconfirmed swap, not too early", + accelerations: []tx{ + {secondsBeforeNow: minTimeBeforeAcceleration + 300, + confirmations: 0}}, + swaps: []tx{ + {secondsBeforeNow: minTimeBeforeAcceleration + 1000, + confirmations: 2}, + {secondsBeforeNow: minTimeBeforeAcceleration + 800, + confirmations: 2}, + {secondsBeforeNow: minTimeBeforeAcceleration + 500, + confirmations: 0}}, }, { - name: "acceleration after unconfirmed, not too early", - confirmations: []uint64{0, 2, 2, 0}, - isAcceleration: []bool{true, false, false, false}, - secondsBeforeNow: []uint64{ - minTimeBeforeAcceleration + 300, - minTimeBeforeAcceleration + 1000, - minTimeBeforeAcceleration + 800, - minTimeBeforeAcceleration + 500, + name: "acceleration after unconfirmed swap, too early", + accelerations: []tx{ + {secondsBeforeNow: minTimeBeforeAcceleration - 300, + confirmations: 0}}, + swaps: []tx{ + {secondsBeforeNow: minTimeBeforeAcceleration + 1000, + confirmations: 2}, + {secondsBeforeNow: minTimeBeforeAcceleration + 800, + confirmations: 2}, + {secondsBeforeNow: minTimeBeforeAcceleration + 500, + confirmations: 0}}, + expectedReturn: &asset.EarlyAcceleration{ + TimePast: minTimeBeforeAcceleration - 300, + WasAccelerated: true, }, - expectTooEarly: false, - expectTimePast: minTimeBeforeAcceleration + 300, }, { - name: "no accelerations, too early", - confirmations: []uint64{2, 2, 0, 0}, - isAcceleration: []bool{false, false, false, false}, - secondsBeforeNow: []uint64{ - minTimeBeforeAcceleration + 1000, - minTimeBeforeAcceleration + 800, - minTimeBeforeAcceleration - 300, - minTimeBeforeAcceleration - 500, + name: "no accelerations, too early", + accelerations: []tx{}, + swaps: []tx{ + {secondsBeforeNow: minTimeBeforeAcceleration + 1000, + confirmations: 2}, + {secondsBeforeNow: minTimeBeforeAcceleration + 800, + confirmations: 2}, + {secondsBeforeNow: minTimeBeforeAcceleration - 300, + confirmations: 0}, + {secondsBeforeNow: minTimeBeforeAcceleration - 500, + confirmations: 0}}, + expectedReturn: &asset.EarlyAcceleration{ + TimePast: minTimeBeforeAcceleration - 300, + WasAccelerated: false, }, - expectTooEarly: true, - expectWasAcceleration: false, - expectTimePast: minTimeBeforeAcceleration - 300, }, { - name: "acceleration after unconfirmed, too early", - confirmations: []uint64{2, 2, 0, 0}, - isAcceleration: []bool{false, false, false, true}, - secondsBeforeNow: []uint64{ - minTimeBeforeAcceleration + 1000, - minTimeBeforeAcceleration + 800, - minTimeBeforeAcceleration + 500, - minTimeBeforeAcceleration - 300, + name: "only accelerations are unconfirmed, too early", + accelerations: []tx{ + {secondsBeforeNow: minTimeBeforeAcceleration - 300, + confirmations: 0}}, + swaps: []tx{ + {secondsBeforeNow: minTimeBeforeAcceleration + 1000, + confirmations: 2}, + {secondsBeforeNow: minTimeBeforeAcceleration + 800, + confirmations: 2}, + }, + expectedReturn: &asset.EarlyAcceleration{ + TimePast: minTimeBeforeAcceleration - 300, + WasAccelerated: true, }, - expectTooEarly: true, - expectWasAcceleration: true, - expectTimePast: minTimeBeforeAcceleration - 300, }, } for _, test := range tests { - sortedTxChain := make([]*GetTransactionResult, 0, len(test.confirmations)) - accelerationCoins := make([]dex.Bytes, 0, len(test.confirmations)) + swapTxs := make([]*GetTransactionResult, 0, len(test.swaps)) + accelerationTxs := make([]*GetTransactionResult, 0, len(test.accelerations)) now := time.Now().Unix() - for i, confs := range test.confirmations { + + for _, swap := range test.swaps { var txHash chainhash.Hash copy(txHash[:], encode.RandomBytes(32)) - if test.isAcceleration[i] { - accelerationCoins = append(accelerationCoins, toCoinID(&txHash, 0)) - } - sortedTxChain = append(sortedTxChain, &GetTransactionResult{ + swapTxs = append(swapTxs, &GetTransactionResult{ + TxID: txHash.String(), + Confirmations: swap.confirmations, + Time: uint64(now) - swap.secondsBeforeNow, + }) + } + + for _, acceleration := range test.accelerations { + var txHash chainhash.Hash + copy(txHash[:], encode.RandomBytes(32)) + accelerationTxs = append(accelerationTxs, &GetTransactionResult{ TxID: txHash.String(), - Confirmations: confs, - Time: uint64(now) - test.secondsBeforeNow[i], + Confirmations: acceleration.confirmations, + Time: uint64(now) - acceleration.secondsBeforeNow, }) } - tooEarly, wasAcceleration, actionTime, err := tooEarlyToAccelerate(sortedTxChain, accelerationCoins) + + earlyAcceleration, err := tooEarlyToAccelerate(swapTxs, accelerationTxs) if test.expectError { if err == nil { t.Fatalf("%s: expected error but did not get", test.name) @@ -3963,19 +4102,18 @@ func TestTooEarlyToAccelerate(t *testing.T) { t.Fatalf("%s: unexpected error: %v", test.name, err) } - if tooEarly != test.expectTooEarly { - t.Fatalf("%s: too early expected: %v, got %v", test.name, test.expectTooEarly, tooEarly) + if test.expectedReturn == nil && earlyAcceleration == nil { + continue } - - if actionTime > test.expectTimePast+1 || - actionTime < test.expectTimePast-1 { - t.Fatalf("%s: action time expected: %v, got %v", - test.name, test.expectTimePast, actionTime) + if test.expectedReturn == nil && earlyAcceleration != nil { + t.Fatalf("%s: expected return to be nil, but got %+v", test.name, earlyAcceleration) } - - // If it is not too early, it doesn't matter what the wasAcceleration return value is - if tooEarly && wasAcceleration != test.expectWasAcceleration { - t.Fatalf("%s: expect was acceleration %v, but got %v", test.name, test.expectWasAcceleration, wasAcceleration) + if test.expectedReturn != nil && earlyAcceleration == nil { + t.Fatalf("%s: expected return to not be nil, but got nil", test.name) + } + if test.expectedReturn.TimePast != earlyAcceleration.TimePast || + test.expectedReturn.WasAccelerated != earlyAcceleration.WasAccelerated { + t.Fatalf("%s: expected %+v, got %+v", test.name, test.expectedReturn, earlyAcceleration) } } } diff --git a/client/asset/interface.go b/client/asset/interface.go index f5b76288f9..608aa840fa 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -47,7 +47,7 @@ func (wt WalletTrait) IsFeeRater() bool { } // IsAccelerator tests if the WalletTrait has the WalletTraitAccelerator bit set, -// which indicates the presence of a Accelerate method. +// which indicates the presence of an Accelerate method. func (wt WalletTrait) IsAccelerator() bool { return wt&WalletTraitAccelerator != 0 } @@ -382,20 +382,22 @@ type EarlyAcceleration struct { // WasAccelerated is true if the action that took place TimePast seconds // ago was an acceleration. If false, the oldest unmined swap transaction // in the order was submitted TimePast seconds ago. - WasAcclerated bool `json:"wasAccelerated"` + WasAccelerated bool `json:"wasAccelerated"` } // Accelerator is implemented by wallets which support acceleration of the // mining of swap transactions. type Accelerator interface { // AccelerateOrder uses the Child-Pays-For-Parent technique to accelerate a - // chain of swap transactions and previous accelerations. It broadcasts a - // new transaction with a fee high enough so that the average fee of all - // the unconfirmed transactions in the chain and the new transaction will - // have an average fee rate of newFeeRate. requiredForRemainingSwaps is - // passed in to ensure that the new change coin will have enough funds to - // initiate the additional swaps that will be required to complete the - // order. + // chain of swap transactions and previous accelerations. It broadcasts a new + // transaction with a fee high enough so that the average fee of all the + // unconfirmed transactions in the chain and the new transaction will have + // an average fee rate of newFeeRate. The changeCoin argument is the latest + // chhange in the order. It must be the input in the acceleration transaction + // in order for the order to be accelerated. requiredForRemainingSwaps is the + // amount of funds required to complete the rest of the swaps in the order. + // The change output of the acceleration transaction will have at least + // this amount. // // The returned change coin may be nil, and should be checked before use. AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (Coin, string, error) @@ -405,12 +407,14 @@ type Accelerator interface { // average fee rate to the desired amount. AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) // PreAccelerate returns the current average fee rate of the unmined swap - // initiation and acceleration transactions, a suggested - // range that the fee rate should be increased to in order to expedite - // mining, and also optionally an EarlyAcceleration notification if - // the user's previous acceleration on this order or the earliest - // unmined transaction in this order happened very recently. - PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, XYRange, *EarlyAcceleration, error) + // initiation and acceleration transactions, and also returns a suggested + // range that the fee rate should be increased to in order to expedite mining. + // The feeSuggestion argument is the current prevailing network rate. It is + // used to help determine the suggestedRange, which is a range meant to give + // the user a good amount of flexibility in determining the post acceleration + // effective fee rate, but still not allowing them to pick something + // outrageously high. + PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, *XYRange, *EarlyAcceleration, error) } // TokenMaster is implemented by assets which support degenerate tokens. diff --git a/client/core/core.go b/client/core/core.go index 727d1b53cb..d1071da5f2 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -7136,7 +7136,6 @@ func (c *Core) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (st if err != nil { return "", err } - tracker, err := c.findActiveOrder(oid) if err != nil { return "", err @@ -7226,10 +7225,15 @@ func (c *Core) PreAccelerateOrder(oidB dex.Bytes) (*PreAccelerate, error) { return nil, err } + if suggestedRange == nil { + // this should never happen + return nil, fmt.Errorf("suggested range is nil") + } + return &PreAccelerate{ SwapRate: currentRate, SuggestedRate: feeSuggestion, - SuggestedRange: suggestedRange, + SuggestedRange: *suggestedRange, EarlyAcceleration: earlyAcceleration, }, nil } diff --git a/client/core/core_test.go b/client/core/core_test.go index 5e2540c801..4b5596757d 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -904,9 +904,9 @@ func (w *TXCWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, ch return nil, w.newAccelerationTxID, nil } -func (w *TXCWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { +func (w *TXCWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, *asset.XYRange, *asset.EarlyAcceleration, error) { if w.accelerateOrderErr != nil { - return 0, asset.XYRange{}, nil, w.accelerateOrderErr + return 0, nil, nil, w.accelerateOrderErr } w.accelerationParams = &struct { @@ -924,7 +924,7 @@ func (w *TXCWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, chan feeSuggestion: feeSuggestion, } - return w.preAccelerateSwapRate, w.preAccelerateSuggestedRange, nil, nil + return w.preAccelerateSwapRate, &w.preAccelerateSuggestedRange, nil, nil } func (w *TXCWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) { diff --git a/client/core/trade.go b/client/core/trade.go index bee8e1ca6d..4be46424f6 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -2843,18 +2843,15 @@ func (t *trackedTrade) orderAccelerationParameters() (swapCoins, accelerationCoi swapCoins = make([]dex.Bytes, 0, len(t.matches)) for _, match := range t.matches { - if match.Status < order.MakerSwapCast { - continue - } var swapCoinID order.CoinID - if match.Side == order.Maker { + if match.Side == order.Maker && match.Status >= order.MakerSwapCast { swapCoinID = match.MetaData.Proof.MakerSwap - } else { - if match.Status < order.TakerSwapCast { - continue - } + } else if match.Side == order.Taker && match.Status >= order.TakerSwapCast { swapCoinID = match.MetaData.Proof.TakerSwap + } else { + continue } + swapCoins = append(swapCoins, dex.Bytes(swapCoinID)) } diff --git a/client/core/wallet.go b/client/core/wallet.go index c709b4892c..352be49f1a 100644 --- a/client/core/wallet.go +++ b/client/core/wallet.go @@ -325,10 +325,10 @@ func (w *xcWallet) accelerationEstimate(swapCoins, accelerationCoins []dex.Bytes // preAccelerate gives the user information about accelerating an order if the // wallet is an Accelerator. -func (w *xcWallet) preAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, asset.XYRange, *asset.EarlyAcceleration, error) { +func (w *xcWallet) preAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, *asset.XYRange, *asset.EarlyAcceleration, error) { accelerator, ok := w.Wallet.(asset.Accelerator) if !ok { - return 0, asset.XYRange{}, nil, errors.New("wallet does not support acceleration") + return 0, &asset.XYRange{}, nil, errors.New("wallet does not support acceleration") } return accelerator.PreAccelerate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion) diff --git a/client/db/bolt/db.go b/client/db/bolt/db.go index 0d640afd49..cfe5a44534 100644 --- a/client/db/bolt/db.go +++ b/client/db/bolt/db.go @@ -1013,7 +1013,7 @@ func decodeOrderBucket(oid []byte, oBkt *bbolt.Bucket) (*dexdb.MetaOrder, error) } var accelerationCoinIDs []order.CoinID - accelerationsB := oBkt.Get(accelerationsKey) + accelerationsB := getCopy(oBkt, accelerationsKey) if len(accelerationsB) > 0 { _, coinIDs, err := encode.DecodeBlob(accelerationsB) if err != nil { From 7ed84e88a1d1683826ea4ccaf3a7d8723b73703d Mon Sep 17 00:00:00 2001 From: martonp Date: Thu, 19 May 2022 10:14:22 +0700 Subject: [PATCH 12/13] Minor updates based on chappjc review --- client/asset/btc/btc.go | 15 +++++++-------- client/asset/btc/btc_test.go | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 2fd30a329b..93b9ac917a 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -590,7 +590,6 @@ type baseWallet struct { estimateFee func(RawRequester, uint64) (uint64, error) // TODO: resolve the awkwardness of an RPC-oriented func in a generic framework decodeAddr dexbtc.AddressDecoder stringAddr dexbtc.AddressStringer - net dex.Network tipMtx sync.RWMutex currentTip *block @@ -871,7 +870,6 @@ func newUnconnectedWallet(cfg *BTCCloneCFG, walletCfg *WalletConfig) (*baseWalle decodeAddr: addrDecoder, stringAddr: addrStringer, walletInfo: cfg.WalletInfo, - net: cfg.Network, } if w.estimateFee == nil { @@ -1965,9 +1963,6 @@ func (btc *baseWallet) lookupWalletTxOutput(txHash *chainhash.Hash, vout uint32) // returned slice will be in the same order as the argument. func (btc *baseWallet) getTransactions(coins []dex.Bytes) ([]*GetTransactionResult, error) { txs := make([]*GetTransactionResult, 0, len(coins)) - if len(coins) == 0 { - return txs, nil - } for _, coinID := range coins { txHash, _, err := decodeCoinID(coinID) @@ -2007,6 +2002,11 @@ func (btc *baseWallet) getTxFee(tx *wire.MsgTx) (uint64, error) { in += uint64(prevMsgTx.TxOut[int(txIn.PreviousOutPoint.Index)].Value) } + if in < out { + return 0, fmt.Errorf("tx %x has value of inputs %d < value of outputs %d", + tx.TxHash(), in, out) + } + return in - out, nil } @@ -2067,10 +2067,9 @@ func (btc *baseWallet) changeCanBeAccelerated(change *output, remainingSwaps boo if utxo.TxID == changeTxHash && utxo.Vout == change.pt.vout { if !remainingSwaps { return errors.New("change locked by another order") - } else { - // change is locked by this order - return nil } + // change is locked by this order + return nil } } diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index bc00a1da6c..b536b1b11c 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -3938,6 +3938,25 @@ func testGetTxFee(t *testing.T, segwit bool, walletType string) { }, expectErr: true, }, + { + name: "tx out > in error", + tx: &wire.MsgTx{ + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: inputTx.TxHash(), + Index: 0, + }}, + {PreviousOutPoint: wire.OutPoint{ + Hash: inputTx.TxHash(), + Index: 2, + }}, + }, + TxOut: []*wire.TxOut{{ + Value: 8e6, + }}, + }, + expectErr: true, + }, } for _, test := range tests { From 52c5d5b6596162d58504299bc76e058b7ed7b4a5 Mon Sep 17 00:00:00 2001 From: martonp Date: Fri, 20 May 2022 20:00:14 +0700 Subject: [PATCH 13/13] Minor updates based on JoeGruffins review --- client/asset/btc/btc.go | 6 +++--- client/asset/interface.go | 2 +- client/webserver/site/src/html/order.tmpl | 4 ++-- client/webserver/site/src/localized_html/en-US/order.tmpl | 4 ++-- client/webserver/site/src/localized_html/pl-PL/order.tmpl | 4 ++-- client/webserver/site/src/localized_html/pt-BR/order.tmpl | 4 ++-- client/webserver/site/src/localized_html/zh-CN/order.tmpl | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 93b9ac917a..bb652f80f6 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -614,7 +614,7 @@ type ExchangeWalletFullNode struct { } // ExchangeWalletAccelerator implements the Accelerator interface on an -// ExchangeWAlletFullNode. +// ExchangeWalletFullNode. type ExchangeWalletAccelerator struct { *ExchangeWalletFullNode } @@ -2186,7 +2186,7 @@ func (btc *baseWallet) signedAccelerationTx(previousTxs []*GetTransactionResult, // transaction with a fee high enough so that the average fee of all the // unconfirmed transactions in the chain and the new transaction will have // an average fee rate of newFeeRate. The changeCoin argument is the latest -// chhange in the order. It must be the input in the acceleration transaction +// change in the order. It must be the input in the acceleration transaction // in order for the order to be accelerated. requiredForRemainingSwaps is the // amount of funds required to complete the rest of the swaps in the order. // The change output of the acceleration transaction will have at least @@ -2202,7 +2202,7 @@ func (btc *ExchangeWalletAccelerator) AccelerateOrder(swapCoins, accelerationCoi // transaction with a fee high enough so that the average fee of all the // unconfirmed transactions in the chain and the new transaction will have // an average fee rate of newFeeRate. The changeCoin argument is the latest -// chhange in the order. It must be the input in the acceleration transaction +// change in the order. It must be the input in the acceleration transaction // in order for the order to be accelerated. requiredForRemainingSwaps is the // amount of funds required to complete the rest of the swaps in the order. // The change output of the acceleration transaction will have at least diff --git a/client/asset/interface.go b/client/asset/interface.go index 608aa840fa..8cad6462ac 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -393,7 +393,7 @@ type Accelerator interface { // transaction with a fee high enough so that the average fee of all the // unconfirmed transactions in the chain and the new transaction will have // an average fee rate of newFeeRate. The changeCoin argument is the latest - // chhange in the order. It must be the input in the acceleration transaction + // change in the order. It must be the input in the acceleration transaction // in order for the order to be accelerated. requiredForRemainingSwaps is the // amount of funds required to complete the rest of the swaps in the order. // The change output of the acceleration transaction will have at least diff --git a/client/webserver/site/src/html/order.tmpl b/client/webserver/site/src/html/order.tmpl index 20e65226ab..51f1c69cd3 100644 --- a/client/webserver/site/src/html/order.tmpl +++ b/client/webserver/site/src/html/order.tmpl @@ -204,7 +204,7 @@
[[[Funding Coins]]]
{{range $ord.FundingCoins}} - {{.StringID}}
+ {{.StringID}}
{{end}}
@@ -213,7 +213,7 @@
[[[acceleration_transactions]]]
{{range $ord.AccelerationCoins}} - {{.StringID}}
+ {{.StringID}}
{{end}}
diff --git a/client/webserver/site/src/localized_html/en-US/order.tmpl b/client/webserver/site/src/localized_html/en-US/order.tmpl index 320c75a3e3..be8bf2d151 100644 --- a/client/webserver/site/src/localized_html/en-US/order.tmpl +++ b/client/webserver/site/src/localized_html/en-US/order.tmpl @@ -204,7 +204,7 @@
Funding Coins
{{range $ord.FundingCoins}} - {{.StringID}}
+ {{.StringID}}
{{end}}
@@ -213,7 +213,7 @@
Acceleration Transactions
{{range $ord.AccelerationCoins}} - {{.StringID}}
+ {{.StringID}}
{{end}}
diff --git a/client/webserver/site/src/localized_html/pl-PL/order.tmpl b/client/webserver/site/src/localized_html/pl-PL/order.tmpl index 55cb828878..13c3f14547 100644 --- a/client/webserver/site/src/localized_html/pl-PL/order.tmpl +++ b/client/webserver/site/src/localized_html/pl-PL/order.tmpl @@ -204,7 +204,7 @@
Środki fundujące zlecenie
{{range $ord.FundingCoins}} - {{.StringID}}
+ {{.StringID}}
{{end}}
@@ -213,7 +213,7 @@
Acceleration Transactions
{{range $ord.AccelerationCoins}} - {{.StringID}}
+ {{.StringID}}
{{end}}
diff --git a/client/webserver/site/src/localized_html/pt-BR/order.tmpl b/client/webserver/site/src/localized_html/pt-BR/order.tmpl index 71bbf6b956..6134c7b1d2 100644 --- a/client/webserver/site/src/localized_html/pt-BR/order.tmpl +++ b/client/webserver/site/src/localized_html/pt-BR/order.tmpl @@ -204,7 +204,7 @@
Moedas de Financiamento
{{range $ord.FundingCoins}} - {{.StringID}}
+ {{.StringID}}
{{end}}
@@ -213,7 +213,7 @@
Acceleration Transactions
{{range $ord.AccelerationCoins}} - {{.StringID}}
+ {{.StringID}}
{{end}}
diff --git a/client/webserver/site/src/localized_html/zh-CN/order.tmpl b/client/webserver/site/src/localized_html/zh-CN/order.tmpl index 2f6cd76569..6a2ef551df 100644 --- a/client/webserver/site/src/localized_html/zh-CN/order.tmpl +++ b/client/webserver/site/src/localized_html/zh-CN/order.tmpl @@ -204,7 +204,7 @@
资金硬币
{{range $ord.FundingCoins}} - {{.StringID}}
+ {{.StringID}}
{{end}}
@@ -213,7 +213,7 @@
Acceleration Transactions
{{range $ord.AccelerationCoins}} - {{.StringID}}
+ {{.StringID}}
{{end}}