decred.org/dcrdex@v1.0.3/client/asset/dcr/dcr.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package dcr 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/sha256" 10 "encoding/base64" 11 "encoding/binary" 12 "encoding/hex" 13 "encoding/json" 14 "errors" 15 "fmt" 16 "io" 17 "math" 18 "net/http" 19 neturl "net/url" 20 "os" 21 "path/filepath" 22 "regexp" 23 "sort" 24 "strconv" 25 "strings" 26 "sync" 27 "sync/atomic" 28 "time" 29 30 "decred.org/dcrdex/client/asset" 31 "decred.org/dcrdex/client/asset/btc" 32 "decred.org/dcrdex/dex" 33 "decred.org/dcrdex/dex/calc" 34 "decred.org/dcrdex/dex/config" 35 "decred.org/dcrdex/dex/dexnet" 36 dexdcr "decred.org/dcrdex/dex/networks/dcr" 37 walletjson "decred.org/dcrwallet/v4/rpc/jsonrpc/types" 38 "decred.org/dcrwallet/v4/wallet" 39 _ "decred.org/dcrwallet/v4/wallet/drivers/bdb" 40 "github.com/decred/dcrd/blockchain/stake/v5" 41 blockchain "github.com/decred/dcrd/blockchain/standalone/v2" 42 "github.com/decred/dcrd/chaincfg/chainhash" 43 "github.com/decred/dcrd/chaincfg/v3" 44 "github.com/decred/dcrd/dcrec" 45 "github.com/decred/dcrd/dcrec/secp256k1/v4" 46 "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" 47 "github.com/decred/dcrd/dcrutil/v4" 48 "github.com/decred/dcrd/hdkeychain/v3" 49 chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4" 50 "github.com/decred/dcrd/txscript/v4" 51 "github.com/decred/dcrd/txscript/v4/sign" 52 "github.com/decred/dcrd/txscript/v4/stdaddr" 53 "github.com/decred/dcrd/txscript/v4/stdscript" 54 "github.com/decred/dcrd/wire" 55 vspdjson "github.com/decred/vspd/types/v2" 56 ) 57 58 const ( 59 // The implementation version. This considers the dex/networks package too. 60 version = 0 61 62 // BipID is the BIP-0044 asset ID. 63 BipID = 42 64 65 // defaultFee is the default value for the fallbackfee. 66 defaultFee = 20 67 // defaultFeeRateLimit is the default value for the feeratelimit. 68 defaultFeeRateLimit = 100 69 // defaultRedeemConfTarget is the default redeem transaction confirmation 70 // target in blocks used by estimatesmartfee to get the optimal fee for a 71 // redeem transaction. 72 defaultRedeemConfTarget = 1 73 74 // splitTxBaggage is the total number of additional bytes associated with 75 // using a split transaction to fund a swap. 76 splitTxBaggage = dexdcr.MsgTxOverhead + dexdcr.P2PKHInputSize + 2*dexdcr.P2PKHOutputSize 77 78 walletTypeDcrwRPC = "dcrwalletRPC" 79 walletTypeLegacy = "" // dcrwallet RPC prior to wallet types 80 walletTypeSPV = "SPV" 81 82 // confCheckTimeout is the amount of time allowed to check for 83 // confirmations. If SPV, this might involve pulling a full block. 84 confCheckTimeout = 4 * time.Second 85 86 // acctInternalBranch is the child number used when performing BIP0044 style 87 // hierarchical deterministic key derivation for the internal branch of an 88 // account. 89 acctInternalBranch uint32 = 1 90 91 // freshFeeAge is the expiry age for cached fee rates of external origin, 92 // past which fetchFeeFromOracle should be used to refresh the rate. 93 freshFeeAge = time.Minute 94 95 // requiredRedeemConfirms is the amount of confirms a redeem transaction 96 // needs before the trade is considered confirmed. The redeem is 97 // monitored until this number of confirms is reached. Two to make sure 98 // the block containing the redeem is stakeholder-approved 99 requiredRedeemConfirms = 2 100 101 vspFileName = "vsp.json" 102 103 defaultCSPPMainnet = "mix.decred.org:5760" 104 defaultCSPPTestnet3 = "mix.decred.org:15760" 105 106 ticketSize = dexdcr.MsgTxOverhead + dexdcr.P2PKHInputSize + 2*dexdcr.P2SHOutputSize /* stakesubmission and sstxchanges */ + 32 /* see e.g. RewardCommitmentScript */ 107 minVSPTicketPurchaseSize = dexdcr.MsgTxOverhead + dexdcr.P2PKHInputSize + dexdcr.P2PKHOutputSize + ticketSize 108 ) 109 110 var ( 111 // ContractSearchLimit is how far back in time AuditContract in SPV mode 112 // will search for a contract if no txData is provided. This should be a 113 // positive duration. 114 ContractSearchLimit = 48 * time.Hour 115 116 // blockTicker is the delay between calls to check for new blocks. 117 blockTicker = time.Second 118 peerCountTicker = 5 * time.Second 119 conventionalConversionFactor = float64(dexdcr.UnitInfo.Conventional.ConversionFactor) 120 walletBlockAllowance = time.Second * 10 121 122 // maxRedeemMempoolAge is the max amount of time the wallet will let a 123 // redeem transaction sit in mempool from the time it is first seen 124 // until it attempts to abandon it and try to send a new transaction. 125 // This is necessary because transactions with already spent inputs may 126 // be tried over and over with wallet in SPV mode. 127 maxRedeemMempoolAge = time.Hour * 2 128 129 walletOpts = []*asset.ConfigOption{ 130 { 131 Key: "fallbackfee", 132 DisplayName: "Fallback fee rate", 133 Description: "The fee rate to use for fee payment and withdrawals when " + 134 "estimatesmartfee is not available. Units: DCR/kB", 135 DefaultValue: defaultFee * 1000 / 1e8, 136 }, 137 { 138 Key: "feeratelimit", 139 DisplayName: "Highest acceptable fee rate", 140 Description: "This is the highest network fee rate you are willing to " + 141 "pay on swap transactions. If feeratelimit is lower than a market's " + 142 "maxfeerate, you will not be able to trade on that market with this " + 143 "wallet. Units: DCR/kB", 144 DefaultValue: defaultFeeRateLimit * 1000 / 1e8, 145 }, 146 { 147 Key: "redeemconftarget", 148 DisplayName: "Redeem confirmation target", 149 Description: "The target number of blocks for the redeem transaction " + 150 "to get a confirmation. Used to set the transaction's fee rate." + 151 " (default: 1 block)", 152 DefaultValue: defaultRedeemConfTarget, 153 }, 154 { 155 Key: "gaplimit", 156 DisplayName: "Address Gap Limit", 157 Description: "The gap limit for used address discovery", 158 DefaultValue: wallet.DefaultGapLimit, 159 }, 160 { 161 Key: "txsplit", 162 DisplayName: "Pre-size funding inputs", 163 Description: "When placing an order, create a \"split\" transaction to " + 164 "fund the order without locking more of the wallet balance than " + 165 "necessary. Otherwise, excess funds may be reserved to fund the order " + 166 "until the first swap contract is broadcast during match settlement, or " + 167 "the order is canceled. This an extra transaction for which network " + 168 "mining fees are paid.", 169 IsBoolean: true, 170 DefaultValue: true, // cheap fees, helpful for bond reserves, and adjustable at order-time 171 }, 172 { 173 Key: "apifeefallback", 174 DisplayName: "External fee rate estimates", 175 Description: "Allow fee rate estimation from a block explorer API. " + 176 "This is useful as a fallback for SPV wallets and RPC wallets " + 177 "that have recently been started.", 178 IsBoolean: true, 179 DefaultValue: true, 180 }, 181 } 182 183 rpcOpts = []*asset.ConfigOption{ 184 { 185 Key: "account", 186 DisplayName: "Account Name", 187 Description: "Primary dcrwallet account name for trading. If automatic mixing of trading funds is " + 188 "desired, this should be the wallet's mixed account and the other accounts should be set too. " + 189 "See wallet documentation for mixing wallet setup instructions.", 190 }, 191 { 192 Key: "unmixedaccount", 193 DisplayName: "Change Account Name", 194 Description: "dcrwallet change account name. This and the 'Temporary Trading Account' should only be " + 195 "set if mixing is enabled on the wallet. If set, deposit addresses will be from this account and will " + 196 "be mixed before being available to trade.", 197 }, 198 { 199 Key: "tradingaccount", 200 DisplayName: "Temporary Trading Account", 201 Description: "dcrwallet account to temporarily store split tx outputs or change from chained swaps in " + 202 "multi-lot orders. This should only be set if 'Change Account Name' is set.", 203 }, 204 { 205 Key: "username", 206 DisplayName: "RPC Username", 207 Description: "dcrwallet's 'username' setting for JSON-RPC", 208 }, 209 { 210 Key: "password", 211 DisplayName: "RPC Password", 212 Description: "dcrwallet's 'password' setting for JSON-RPC", 213 NoEcho: true, 214 }, 215 { 216 Key: "rpclisten", 217 DisplayName: "RPC Address", 218 Description: "dcrwallet's address (host or host:port) (default port: 9110)", 219 DefaultValue: "127.0.0.1:9110", 220 }, 221 { 222 Key: "rpccert", 223 DisplayName: "TLS Certificate", 224 Description: "Path to the dcrwallet TLS certificate file", 225 DefaultValue: defaultRPCCert, 226 }, 227 } 228 229 multiFundingOpts = []*asset.OrderOption{ 230 { 231 ConfigOption: asset.ConfigOption{ 232 Key: multiSplitKey, 233 DisplayName: "Allow multi split", 234 Description: "Allow split funding transactions that pre-size outputs to " + 235 "prevent excessive overlock.", 236 IsBoolean: true, 237 DefaultValue: true, 238 }, 239 }, 240 { 241 ConfigOption: asset.ConfigOption{ 242 Key: multiSplitBufferKey, 243 DisplayName: "Multi split buffer", 244 Description: "Add an integer percent buffer to split output amounts to " + 245 "facilitate output reuse. This is only required for quote assets.", 246 DefaultValue: 5, 247 DependsOn: multiSplitKey, 248 }, 249 QuoteAssetOnly: true, 250 XYRange: &asset.XYRange{ 251 Start: asset.XYRangePoint{ 252 Label: "0%", 253 X: 0, 254 Y: 0, 255 }, 256 End: asset.XYRangePoint{ 257 Label: "100%", 258 X: 100, 259 Y: 100, 260 }, 261 XUnit: "%", 262 YUnit: "%", 263 RoundX: true, 264 RoundY: true, 265 }, 266 }, 267 } 268 269 // WalletInfo defines some general information about a Decred wallet. 270 WalletInfo = &asset.WalletInfo{ 271 Name: "Decred", 272 SupportedVersions: []uint32{version}, 273 UnitInfo: dexdcr.UnitInfo, 274 AvailableWallets: []*asset.WalletDefinition{ 275 { 276 Type: walletTypeSPV, 277 Tab: "Native", 278 Description: "Use the built-in SPV wallet", 279 ConfigOpts: walletOpts, 280 Seeded: true, 281 MultiFundingOpts: multiFundingOpts, 282 }, 283 { 284 Type: walletTypeDcrwRPC, 285 Tab: "External", 286 Description: "Connect to dcrwallet", 287 DefaultConfigPath: defaultConfigPath, 288 ConfigOpts: append(rpcOpts, walletOpts...), 289 MultiFundingOpts: multiFundingOpts, 290 }, 291 }, 292 } 293 swapFeeBumpKey = "swapfeebump" 294 splitKey = "swapsplit" 295 multiSplitKey = "multisplit" 296 multiSplitBufferKey = "multisplitbuffer" 297 redeemFeeBumpFee = "redeemfeebump" 298 client http.Client 299 ) 300 301 // outPoint is the hash and output index of a transaction output. 302 type outPoint struct { 303 txHash chainhash.Hash 304 vout uint32 305 } 306 307 // newOutPoint is the constructor for a new outPoint. 308 func newOutPoint(txHash *chainhash.Hash, vout uint32) outPoint { 309 return outPoint{ 310 txHash: *txHash, 311 vout: vout, 312 } 313 } 314 315 // String is a human-readable string representation of the outPoint. 316 func (pt outPoint) String() string { 317 return pt.txHash.String() + ":" + strconv.Itoa(int(pt.vout)) 318 } 319 320 // output is information about a transaction output. output satisfies the 321 // asset.Coin interface. 322 type output struct { 323 pt outPoint 324 tree int8 325 value uint64 326 } 327 328 // newOutput is the constructor for an output. 329 func newOutput(txHash *chainhash.Hash, vout uint32, value uint64, tree int8) *output { 330 return &output{ 331 pt: outPoint{ 332 txHash: *txHash, 333 vout: vout, 334 }, 335 value: value, 336 tree: tree, 337 } 338 } 339 340 // Value returns the value of the output. Part of the asset.Coin interface. 341 func (op *output) Value() uint64 { 342 return op.value 343 } 344 345 // ID is the output's coin ID. Part of the asset.Coin interface. For DCR, the 346 // coin ID is 36 bytes = 32 bytes tx hash + 4 bytes big-endian vout. 347 func (op *output) ID() dex.Bytes { 348 return toCoinID(op.txHash(), op.vout()) 349 } 350 351 func (op *output) TxID() string { 352 return op.txHash().String() 353 } 354 355 // String is a string representation of the coin. 356 func (op *output) String() string { 357 return op.pt.String() 358 } 359 360 // txHash returns the pointer of the outPoint's txHash. 361 func (op *output) txHash() *chainhash.Hash { 362 return &op.pt.txHash 363 } 364 365 // vout returns the outPoint's vout. 366 func (op *output) vout() uint32 { 367 return op.pt.vout 368 } 369 370 // wireOutPoint creates and returns a new *wire.OutPoint for the output. 371 func (op *output) wireOutPoint() *wire.OutPoint { 372 return wire.NewOutPoint(op.txHash(), op.vout(), op.tree) 373 } 374 375 // auditInfo is information about a swap contract on the blockchain, not 376 // necessarily created by this wallet, as would be returned from AuditContract. 377 type auditInfo struct { 378 output *output 379 secretHash []byte 380 contract []byte 381 recipient stdaddr.Address // unused? 382 expiration time.Time 383 } 384 385 // Expiration is the expiration time of the contract, which is the earliest time 386 // that a refund can be issued for an un-redeemed contract. 387 func (ci *auditInfo) Expiration() time.Time { 388 return ci.expiration 389 } 390 391 // Contract is the contract script. 392 func (ci *auditInfo) Contract() dex.Bytes { 393 return ci.contract 394 } 395 396 // Coin returns the output as an asset.Coin. 397 func (ci *auditInfo) Coin() asset.Coin { 398 return ci.output 399 } 400 401 // SecretHash is the contract's secret hash. 402 func (ci *auditInfo) SecretHash() dex.Bytes { 403 return ci.secretHash 404 } 405 406 // convertAuditInfo converts from the common *asset.AuditInfo type to our 407 // internal *auditInfo type. 408 func convertAuditInfo(ai *asset.AuditInfo, chainParams *chaincfg.Params) (*auditInfo, error) { 409 if ai.Coin == nil { 410 return nil, fmt.Errorf("no coin") 411 } 412 413 op, ok := ai.Coin.(*output) 414 if !ok { 415 return nil, fmt.Errorf("unknown coin type %T", ai.Coin) 416 } 417 418 recip, err := stdaddr.DecodeAddress(ai.Recipient, chainParams) 419 if err != nil { 420 return nil, err 421 } 422 423 return &auditInfo{ 424 output: op, // *output 425 recipient: recip, // btcutil.Address 426 contract: ai.Contract, // []byte 427 secretHash: ai.SecretHash, // []byte 428 expiration: ai.Expiration, // time.Time 429 }, nil 430 } 431 432 // swapReceipt is information about a swap contract that was broadcast by this 433 // wallet. Satisfies the asset.Receipt interface. 434 type swapReceipt struct { 435 output *output 436 contract []byte 437 signedRefund []byte 438 expiration time.Time 439 } 440 441 // Expiration is the time that the contract will expire, allowing the user to 442 // issue a refund transaction. Part of the asset.Receipt interface. 443 func (r *swapReceipt) Expiration() time.Time { 444 return r.expiration 445 } 446 447 // Coin is the contract script. Part of the asset.Receipt interface. 448 func (r *swapReceipt) Contract() dex.Bytes { 449 return r.contract 450 } 451 452 // Coin is the output information as an asset.Coin. Part of the asset.Receipt 453 // interface. 454 func (r *swapReceipt) Coin() asset.Coin { 455 return r.output 456 } 457 458 // String provides a human-readable representation of the contract's Coin. 459 func (r *swapReceipt) String() string { 460 return r.output.String() 461 } 462 463 // SignedRefund is a signed refund script that can be used to return 464 // funds to the user in the case a contract expires. 465 func (r *swapReceipt) SignedRefund() dex.Bytes { 466 return r.signedRefund 467 } 468 469 // fundingCoin is similar to output, but also stores the address. The 470 // ExchangeWallet fundingCoins dict is used as a local cache of coins being 471 // spent. 472 type fundingCoin struct { 473 op *output 474 addr string 475 } 476 477 // Driver implements asset.Driver. 478 type Driver struct{} 479 480 // Check that Driver implements asset.Driver. 481 var _ asset.Driver = (*Driver)(nil) 482 var _ asset.Creator = (*Driver)(nil) 483 484 // Open creates the DCR exchange wallet. Start the wallet with its Run method. 485 func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) { 486 return NewWallet(cfg, logger, network) 487 } 488 489 // DecodeCoinID creates a human-readable representation of a coin ID for Decred. 490 func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { 491 txid, vout, err := decodeCoinID(coinID) 492 if err != nil { 493 return "<invalid>", err 494 } 495 return fmt.Sprintf("%v:%d", txid, vout), err 496 } 497 498 // Info returns basic information about the wallet and asset. 499 func (d *Driver) Info() *asset.WalletInfo { 500 return WalletInfo 501 } 502 503 // Exists checks the existence of the wallet. Part of the Creator interface. 504 func (d *Driver) Exists(walletType, dataDir string, _ map[string]string, net dex.Network) (bool, error) { 505 if walletType != walletTypeSPV { 506 return false, fmt.Errorf("no Decred wallet of type %q available", walletType) 507 } 508 509 chainParams, err := parseChainParams(net) 510 if err != nil { 511 return false, err 512 } 513 514 return walletExists(filepath.Join(dataDir, chainParams.Name, "spv")) 515 } 516 517 // Create creates a new SPV wallet. 518 func (d *Driver) Create(params *asset.CreateWalletParams) error { 519 if params.Type != walletTypeSPV { 520 return fmt.Errorf("SPV is the only seeded wallet type. required = %q, requested = %q", walletTypeSPV, params.Type) 521 } 522 if len(params.Seed) == 0 { 523 return errors.New("wallet seed cannot be empty") 524 } 525 if len(params.DataDir) == 0 { 526 return errors.New("must specify wallet data directory") 527 } 528 chainParams, err := parseChainParams(params.Net) 529 if err != nil { 530 return fmt.Errorf("error parsing chain params: %w", err) 531 } 532 533 recoveryCfg := new(RecoveryCfg) 534 err = config.Unmapify(params.Settings, recoveryCfg) 535 if err != nil { 536 return err 537 } 538 539 return createSPVWallet(params.Pass, params.Seed, params.DataDir, recoveryCfg.NumExternalAddresses, 540 recoveryCfg.NumInternalAddresses, recoveryCfg.GapLimit, chainParams) 541 } 542 543 // MinLotSize calculates the minimum bond size for a given fee rate that avoids 544 // dust outputs on the swap and refund txs, assuming the maxFeeRate doesn't 545 // change. 546 func (d *Driver) MinLotSize(maxFeeRate uint64) uint64 { 547 return dexdcr.MinLotSize(maxFeeRate) 548 } 549 550 func init() { 551 asset.Register(BipID, &Driver{}) 552 } 553 554 // RecoveryCfg is the information that is transferred from the old wallet 555 // to the new one when the wallet is recovered. 556 type RecoveryCfg struct { 557 NumExternalAddresses uint32 `ini:"numexternaladdr"` 558 NumInternalAddresses uint32 `ini:"numinternaladdr"` 559 GapLimit uint32 `ini:"gaplimit"` 560 } 561 562 // swapOptions captures the available Swap options. Tagged to be used with 563 // config.Unmapify to decode e.g. asset.Order.Options. 564 type swapOptions struct { 565 Split *bool `ini:"swapsplit"` 566 FeeBump *float64 `ini:"swapfeebump"` 567 } 568 569 func (s *swapOptions) feeBump() (float64, error) { 570 bump := 1.0 571 if s.FeeBump != nil { 572 bump = *s.FeeBump 573 if bump > 2.0 { 574 return 0, fmt.Errorf("fee bump %f is higher than the 2.0 limit", bump) 575 } 576 if bump < 1.0 { 577 return 0, fmt.Errorf("fee bump %f is lower than 1", bump) 578 } 579 } 580 return bump, nil 581 } 582 583 // redeemOptions are order options that apply to redemptions. 584 type redeemOptions struct { 585 FeeBump *float64 `ini:"redeemfeebump"` 586 } 587 588 type feeStamped struct { 589 rate uint64 590 stamp time.Time 591 } 592 593 // exchangeWalletConfig is the validated, unit-converted, user-configurable 594 // wallet settings. 595 type exchangeWalletConfig struct { 596 useSplitTx bool 597 fallbackFeeRate uint64 598 feeRateLimit uint64 599 redeemConfTarget uint64 600 apiFeeFallback bool 601 } 602 603 type mempoolRedeem struct { 604 txHash chainhash.Hash 605 firstSeen time.Time 606 } 607 608 // vsp holds info needed for purchasing tickets from a vsp. PubKey is from the 609 // vsp and is used for verifying communications. 610 type vsp struct { 611 URL string `json:"url"` 612 FeePercentage float64 `json:"feepercent"` 613 PubKey string `json:"pubkey"` 614 } 615 616 // rescanProgress is the progress of an asynchronous rescan. 617 type rescanProgress struct { 618 scannedThrough int64 619 } 620 621 // ExchangeWallet is a wallet backend for Decred. The backend is how the DEX 622 // client app communicates with the Decred blockchain and wallet. ExchangeWallet 623 // satisfies the dex.Wallet interface. 624 type ExchangeWallet struct { 625 bondReserves atomic.Uint64 626 cfgV atomic.Value // *exchangeWalletConfig 627 628 ctx context.Context // the asset subsystem starts with Connect(ctx) 629 wg sync.WaitGroup 630 wallet Wallet 631 chainParams *chaincfg.Params 632 log dex.Logger 633 network dex.Network 634 emit *asset.WalletEmitter 635 lastPeerCount uint32 636 peersChange func(uint32, error) 637 vspFilepath string 638 walletType string 639 walletDir string 640 startingBlocks atomic.Uint64 641 642 oracleFeesMtx sync.Mutex 643 oracleFees map[uint64]feeStamped // conf target => fee rate 644 oracleFailing bool 645 646 handleTipMtx sync.Mutex 647 currentTip atomic.Value // *block 648 649 // Coins returned by Fund are cached for quick reference. 650 fundingMtx sync.RWMutex 651 fundingCoins map[outPoint]*fundingCoin 652 653 findRedemptionMtx sync.RWMutex 654 findRedemptionQueue map[outPoint]*findRedemptionReq 655 656 externalTxMtx sync.RWMutex 657 externalTxCache map[chainhash.Hash]*externalTx 658 659 // TODO: Consider persisting mempool redeems on file. 660 mempoolRedeemsMtx sync.RWMutex 661 mempoolRedeems map[[32]byte]*mempoolRedeem // keyed by secret hash 662 663 vspV atomic.Value // *vsp 664 665 connected atomic.Bool 666 667 subsidyCache *blockchain.SubsidyCache 668 669 ticketBuyer struct { 670 running atomic.Bool 671 remaining atomic.Int32 672 unconfirmedTickets map[chainhash.Hash]struct{} 673 } 674 675 // Embedding wallets can set cycleMixer, which will be triggered after 676 // new block are seen. 677 cycleMixer func() 678 mixing atomic.Bool 679 680 pendingTxsMtx sync.RWMutex 681 pendingTxs map[chainhash.Hash]*btc.ExtendedWalletTx 682 683 receiveTxLastQuery atomic.Uint64 684 685 txHistoryDB atomic.Value // *btc.BadgerTxDB 686 syncingTxHistory atomic.Bool 687 688 previouslySynced atomic.Bool 689 690 rescan struct { 691 sync.RWMutex 692 progress *rescanProgress // nil = no rescan in progress 693 } 694 } 695 696 func (dcr *ExchangeWallet) config() *exchangeWalletConfig { 697 return dcr.cfgV.Load().(*exchangeWalletConfig) 698 } 699 700 // Check that ExchangeWallet satisfies the Wallet interface. 701 var _ asset.Wallet = (*ExchangeWallet)(nil) 702 var _ asset.FeeRater = (*ExchangeWallet)(nil) 703 var _ asset.Withdrawer = (*ExchangeWallet)(nil) 704 var _ asset.LiveReconfigurer = (*ExchangeWallet)(nil) 705 var _ asset.TxFeeEstimator = (*ExchangeWallet)(nil) 706 var _ asset.Bonder = (*ExchangeWallet)(nil) 707 var _ asset.Authenticator = (*ExchangeWallet)(nil) 708 var _ asset.TicketBuyer = (*ExchangeWallet)(nil) 709 var _ asset.WalletHistorian = (*ExchangeWallet)(nil) 710 711 type block struct { 712 height int64 713 hash *chainhash.Hash 714 } 715 716 // findRedemptionReq represents a request to find a contract's redemption, 717 // which is added to the findRedemptionQueue with the contract outpoint as 718 // key. 719 type findRedemptionReq struct { 720 ctx context.Context 721 contractP2SHScript []byte 722 contractOutputScriptVer uint16 723 resultChan chan *findRedemptionResult 724 } 725 726 func (frr *findRedemptionReq) canceled() bool { 727 return frr.ctx.Err() != nil 728 } 729 730 // findRedemptionResult models the result of a find redemption attempt. 731 type findRedemptionResult struct { 732 RedemptionCoinID dex.Bytes 733 Secret dex.Bytes 734 Err error 735 } 736 737 // NewWallet is the exported constructor by which the DEX will import the 738 // exchange wallet. 739 func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) { 740 // loadConfig will set fields if defaults are used and set the chainParams 741 // variable. 742 walletCfg := new(walletConfig) 743 chainParams, err := loadConfig(cfg.Settings, network, walletCfg) 744 if err != nil { 745 return nil, err 746 } 747 748 dcr, err := unconnectedWallet(cfg, walletCfg, chainParams, logger, network) 749 if err != nil { 750 return nil, err 751 } 752 753 var w asset.Wallet = dcr 754 755 switch cfg.Type { 756 case walletTypeDcrwRPC, walletTypeLegacy: 757 dcr.wallet, err = newRPCWallet(cfg.Settings, logger, network) 758 if err != nil { 759 return nil, err 760 } 761 case walletTypeSPV: 762 dcr.wallet, err = openSPVWallet(cfg.DataDir, walletCfg.GapLimit, chainParams, logger) 763 if err != nil { 764 return nil, err 765 } 766 w, err = initNativeWallet(dcr) 767 if err != nil { 768 return nil, err 769 } 770 default: 771 makeCustomWallet, ok := customWalletConstructors[cfg.Type] 772 if !ok { 773 return nil, fmt.Errorf("unknown wallet type %q", cfg.Type) 774 } 775 776 // Create custom wallet and return early if we encounter any error. 777 dcr.wallet, err = makeCustomWallet(cfg.Settings, chainParams, logger) 778 if err != nil { 779 return nil, fmt.Errorf("custom wallet setup error: %v", err) 780 } 781 } 782 783 return w, nil 784 } 785 786 func getExchangeWalletCfg(dcrCfg *walletConfig, logger dex.Logger) (*exchangeWalletConfig, error) { 787 // If set in the user config, the fallback fee will be in units of DCR/kB. 788 // Convert to atoms/B. 789 fallbackFeesPerByte := toAtoms(dcrCfg.FallbackFeeRate / 1000) 790 if fallbackFeesPerByte == 0 { 791 fallbackFeesPerByte = defaultFee 792 } 793 logger.Tracef("Fallback fees set at %d atoms/byte", fallbackFeesPerByte) 794 795 // If set in the user config, the fee rate limit will be in units of DCR/KB. 796 // Convert to atoms/byte & error if value is smaller than smallest unit. 797 feesLimitPerByte := uint64(defaultFeeRateLimit) 798 if dcrCfg.FeeRateLimit > 0 { 799 feesLimitPerByte = toAtoms(dcrCfg.FeeRateLimit / 1000) 800 if feesLimitPerByte == 0 { 801 return nil, fmt.Errorf("Fee rate limit is smaller than smallest unit: %v", 802 dcrCfg.FeeRateLimit) 803 } 804 } 805 logger.Tracef("Fees rate limit set at %d atoms/byte", feesLimitPerByte) 806 807 redeemConfTarget := dcrCfg.RedeemConfTarget 808 if redeemConfTarget == 0 { 809 redeemConfTarget = defaultRedeemConfTarget 810 } 811 logger.Tracef("Redeem conf target set to %d blocks", redeemConfTarget) 812 813 return &exchangeWalletConfig{ 814 fallbackFeeRate: fallbackFeesPerByte, 815 feeRateLimit: feesLimitPerByte, 816 redeemConfTarget: redeemConfTarget, 817 useSplitTx: dcrCfg.UseSplitTx, 818 apiFeeFallback: dcrCfg.ApiFeeFallback, 819 }, nil 820 } 821 822 // unconnectedWallet returns an ExchangeWallet without a base wallet. The wallet 823 // should be set before use. 824 func unconnectedWallet(cfg *asset.WalletConfig, dcrCfg *walletConfig, chainParams *chaincfg.Params, logger dex.Logger, network dex.Network) (*ExchangeWallet, error) { 825 walletCfg, err := getExchangeWalletCfg(dcrCfg, logger) 826 if err != nil { 827 return nil, err 828 } 829 830 dir := filepath.Join(cfg.DataDir, chainParams.Name) 831 if err := os.MkdirAll(dir, 0755); err != nil { 832 return nil, fmt.Errorf("unable to create wallet dir: %v", err) 833 } 834 835 vspFilepath := filepath.Join(dir, vspFileName) 836 837 w := &ExchangeWallet{ 838 log: logger, 839 chainParams: chainParams, 840 network: network, 841 emit: cfg.Emit, 842 peersChange: cfg.PeersChange, 843 fundingCoins: make(map[outPoint]*fundingCoin), 844 findRedemptionQueue: make(map[outPoint]*findRedemptionReq), 845 externalTxCache: make(map[chainhash.Hash]*externalTx), 846 oracleFees: make(map[uint64]feeStamped), 847 mempoolRedeems: make(map[[32]byte]*mempoolRedeem), 848 vspFilepath: vspFilepath, 849 walletType: cfg.Type, 850 subsidyCache: blockchain.NewSubsidyCache(chainParams), 851 pendingTxs: make(map[chainhash.Hash]*btc.ExtendedWalletTx), 852 walletDir: dir, 853 } 854 855 if b, err := os.ReadFile(vspFilepath); err == nil { 856 var v vsp 857 err = json.Unmarshal(b, &v) 858 if err != nil { 859 return nil, fmt.Errorf("unable to unmarshal vsp file: %v", err) 860 } 861 w.vspV.Store(&v) 862 } else if !errors.Is(err, os.ErrNotExist) { 863 return nil, fmt.Errorf("unable to read vsp file: %v", err) 864 } 865 866 w.cfgV.Store(walletCfg) 867 868 return w, nil 869 } 870 871 // openSPVWallet opens the previously created native SPV wallet. 872 func openSPVWallet(dataDir string, gapLimit uint32, chainParams *chaincfg.Params, log dex.Logger) (*spvWallet, error) { 873 dir := filepath.Join(dataDir, chainParams.Name, "spv") 874 if exists, err := walletExists(dir); err != nil { 875 return nil, err 876 } else if !exists { 877 return nil, fmt.Errorf("wallet at %q doesn't exists", dir) 878 } 879 880 return &spvWallet{ 881 dir: dir, 882 chainParams: chainParams, 883 log: log.SubLogger("SPV"), 884 blockCache: blockCache{ 885 blocks: make(map[chainhash.Hash]*cachedBlock), 886 }, 887 tipChan: make(chan *block, 16), 888 gapLimit: gapLimit, 889 }, nil 890 } 891 892 // Info returns basic information about the wallet and asset. 893 func (dcr *ExchangeWallet) Info() *asset.WalletInfo { 894 return WalletInfo 895 } 896 897 // var logup uint32 898 899 // func rpclog(log dex.Logger) { 900 // if atomic.CompareAndSwapUint32(&logup, 0, 1) { 901 // rpcclient.UseLogger(log) 902 // } 903 // } 904 905 func (dcr *ExchangeWallet) txHistoryDBPath(walletID string) string { 906 return filepath.Join(dcr.walletDir, fmt.Sprintf("txhistorydb-%s", walletID)) 907 } 908 909 // findExistingAddressBasedTxHistoryDB finds the path of a tx history db that 910 // was created using an address controlled by the wallet. This should only be 911 // used for RPC wallets, as SPV wallets are able to get the first address 912 // generated by the wallet. 913 func (dcr *ExchangeWallet) findExistingAddressBasedTxHistoryDB() (string, error) { 914 dir, err := os.Open(dcr.walletDir) 915 if err != nil { 916 return "", fmt.Errorf("error opening wallet directory: %w", err) 917 } 918 defer dir.Close() 919 920 entries, err := dir.Readdir(0) 921 if err != nil { 922 return "", fmt.Errorf("error reading wallet directory: %w", err) 923 } 924 925 pattern := regexp.MustCompile(`^txhistorydb-(.+)$`) 926 927 for _, entry := range entries { 928 if !entry.IsDir() { 929 continue 930 } 931 932 match := pattern.FindStringSubmatch(entry.Name()) 933 if match == nil { 934 continue 935 } 936 937 address := match[1] 938 939 decodedAddr, err := stdaddr.DecodeAddress(address, dcr.chainParams) 940 if err != nil { 941 continue 942 } 943 owns, err := dcr.wallet.WalletOwnsAddress(dcr.ctx, decodedAddr) 944 if err != nil { 945 continue 946 } 947 if owns { 948 return filepath.Join(dcr.walletDir, entry.Name()), nil 949 } 950 } 951 952 return "", nil 953 } 954 955 func (dcr *ExchangeWallet) startTxHistoryDB(ctx context.Context) (*dex.ConnectionMaster, error) { 956 var dbPath string 957 if spvWallet, ok := dcr.wallet.(*spvWallet); ok { 958 initialAddress, err := spvWallet.InitialAddress(ctx) 959 if err != nil { 960 return nil, err 961 } 962 963 dbPath = dcr.txHistoryDBPath(initialAddress) 964 } 965 966 if dbPath == "" { 967 addressPath, err := dcr.findExistingAddressBasedTxHistoryDB() 968 if err != nil { 969 return nil, err 970 } 971 if addressPath != "" { 972 dbPath = addressPath 973 } 974 } 975 976 if dbPath == "" { 977 depositAddr, err := dcr.DepositAddress() 978 if err != nil { 979 return nil, fmt.Errorf("error getting deposit address: %w", err) 980 } 981 dbPath = dcr.txHistoryDBPath(depositAddr) 982 } 983 984 dcr.log.Debugf("Using tx history db at %s", dbPath) 985 986 db := btc.NewBadgerTxDB(dbPath, dcr.log) 987 dcr.txHistoryDB.Store(db) 988 989 cm := dex.NewConnectionMaster(db) 990 if err := cm.ConnectOnce(ctx); err != nil { 991 return nil, fmt.Errorf("error connecting to tx history db: %w", err) 992 } 993 994 var success bool 995 defer func() { 996 if !success { 997 cm.Disconnect() 998 } 999 }() 1000 1001 pendingTxs, err := db.GetPendingTxs() 1002 if err != nil { 1003 return nil, fmt.Errorf("failed to load unconfirmed txs: %v", err) 1004 } 1005 1006 dcr.pendingTxsMtx.Lock() 1007 for _, tx := range pendingTxs { 1008 txHash, err := chainhash.NewHashFromStr(tx.ID) 1009 if err != nil { 1010 dcr.log.Errorf("Invalid txid %v from tx history db: %v", tx.ID, err) 1011 continue 1012 } 1013 dcr.pendingTxs[*txHash] = tx 1014 } 1015 dcr.pendingTxsMtx.Unlock() 1016 1017 lastQuery, err := db.GetLastReceiveTxQuery() 1018 if errors.Is(err, btc.ErrNeverQueried) { 1019 lastQuery = 0 1020 } else if err != nil { 1021 return nil, fmt.Errorf("failed to load last query time: %v", err) 1022 } 1023 1024 dcr.receiveTxLastQuery.Store(lastQuery) 1025 1026 success = true 1027 return cm, nil 1028 } 1029 1030 // Connect connects the wallet to the RPC server. Satisfies the dex.Connector 1031 // interface. 1032 func (dcr *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) { 1033 // rpclog(dcr.log) 1034 dcr.ctx = ctx 1035 1036 err := dcr.wallet.Connect(ctx) 1037 if err != nil { 1038 return nil, err 1039 } 1040 1041 // The wallet is connected now, so if any of the following checks 1042 // fails and we return with a non-nil error, we must disconnect the 1043 // wallet. 1044 // This is especially important as the wallet may be using an rpc 1045 // connection which was established above and if we do not disconnect, 1046 // subsequent reconnect attempts will be met with "websocket client 1047 // has already connected". 1048 var success bool 1049 defer func() { 1050 if !success { 1051 dcr.wallet.Disconnect() 1052 } 1053 }() 1054 1055 // Validate accounts early on to prevent errors later. 1056 for _, acct := range dcr.allAccounts() { 1057 if acct == "" { 1058 continue 1059 } 1060 _, err = dcr.wallet.AccountUnlocked(ctx, acct) 1061 if err != nil { 1062 return nil, fmt.Errorf("unexpected AccountUnlocked error for %q account: %w", acct, err) 1063 } 1064 } 1065 1066 // Initialize the best block. 1067 tip, err := dcr.getBestBlock(ctx) 1068 if err != nil { 1069 return nil, fmt.Errorf("error initializing best block for DCR: %w", err) 1070 } 1071 dcr.currentTip.Store(tip) 1072 dcr.startingBlocks.Store(uint64(tip.height)) 1073 1074 dbCM, err := dcr.startTxHistoryDB(ctx) 1075 if err != nil { 1076 return nil, err 1077 } 1078 1079 success = true // All good, don't disconnect the wallet when this method returns. 1080 dcr.connected.Store(true) 1081 1082 dcr.wg.Add(1) 1083 go func() { 1084 defer dcr.wg.Done() 1085 defer dbCM.Disconnect() 1086 dcr.monitorBlocks(ctx) 1087 dcr.shutdown() 1088 }() 1089 1090 dcr.wg.Add(1) 1091 go func() { 1092 defer dcr.wg.Done() 1093 dcr.monitorPeers(ctx) 1094 }() 1095 1096 dcr.wg.Add(1) 1097 go func() { 1098 defer dcr.wg.Done() 1099 dcr.syncTxHistory(ctx, uint64(tip.height)) 1100 }() 1101 1102 return &dcr.wg, nil 1103 } 1104 1105 // Reconfigure attempts to reconfigure the wallet. 1106 func (dcr *ExchangeWallet) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, currentAddress string) (restart bool, err error) { 1107 dcrCfg := new(walletConfig) 1108 _, err = loadConfig(cfg.Settings, dcr.network, dcrCfg) 1109 if err != nil { 1110 return false, err 1111 } 1112 1113 restart, err = dcr.wallet.Reconfigure(ctx, cfg, dcr.network, currentAddress) 1114 if err != nil || restart { 1115 return restart, err 1116 } 1117 1118 exchangeWalletCfg, err := getExchangeWalletCfg(dcrCfg, dcr.log) 1119 if err != nil { 1120 return false, err 1121 } 1122 dcr.cfgV.Store(exchangeWalletCfg) 1123 return false, nil 1124 } 1125 1126 // depositAccount returns the account that may be used to receive funds into 1127 // the wallet, either by a direct deposit action or via redemption or refund. 1128 func (dcr *ExchangeWallet) depositAccount() string { 1129 accts := dcr.wallet.Accounts() 1130 if accts.UnmixedAccount != "" { 1131 return accts.UnmixedAccount 1132 } 1133 return accts.PrimaryAccount 1134 } 1135 1136 // fundingAccounts returns the primary account along with any configured trading 1137 // account which may contain spendable outputs (split tx outputs or chained swap 1138 // change). 1139 func (dcr *ExchangeWallet) fundingAccounts() []string { 1140 accts := dcr.wallet.Accounts() 1141 if accts.UnmixedAccount == "" { 1142 return []string{accts.PrimaryAccount} 1143 } 1144 return []string{accts.PrimaryAccount, accts.TradingAccount} 1145 } 1146 1147 func (dcr *ExchangeWallet) allAccounts() []string { 1148 accts := dcr.wallet.Accounts() 1149 if accts.UnmixedAccount == "" { 1150 return []string{accts.PrimaryAccount} 1151 } 1152 return []string{accts.PrimaryAccount, accts.TradingAccount, accts.UnmixedAccount} 1153 } 1154 1155 // OwnsDepositAddress indicates if the provided address can be used to deposit 1156 // funds into the wallet. 1157 func (dcr *ExchangeWallet) OwnsDepositAddress(address string) (bool, error) { 1158 addr, err := stdaddr.DecodeAddress(address, dcr.chainParams) 1159 if err != nil { 1160 return false, err 1161 } 1162 return dcr.wallet.AccountOwnsAddress(dcr.ctx, addr, dcr.depositAccount()) 1163 } 1164 1165 func (dcr *ExchangeWallet) balance() (*asset.Balance, error) { 1166 accts := dcr.wallet.Accounts() 1167 1168 locked, err := dcr.lockedAtoms(accts.PrimaryAccount) 1169 if err != nil { 1170 return nil, err 1171 } 1172 ab, err := dcr.wallet.AccountBalance(dcr.ctx, 0, accts.PrimaryAccount) 1173 if err != nil { 1174 return nil, err 1175 } 1176 bal := &asset.Balance{ 1177 Available: toAtoms(ab.Spendable) - locked, 1178 Immature: toAtoms(ab.ImmatureCoinbaseRewards) + 1179 toAtoms(ab.ImmatureStakeGeneration), 1180 Locked: locked + toAtoms(ab.LockedByTickets), 1181 Other: make(map[asset.BalanceCategory]asset.CustomBalance), 1182 } 1183 1184 bal.Other[asset.BalanceCategoryStaked] = asset.CustomBalance{ 1185 Amount: toAtoms(ab.LockedByTickets), 1186 } 1187 1188 if accts.UnmixedAccount == "" { 1189 return bal, nil 1190 } 1191 1192 // Mixing is enabled, consider ... 1193 // 1) trading account spendable (-locked) as available, 1194 // 2) all unmixed funds as immature, and 1195 // 3) all locked utxos in the trading account as locked (for swapping). 1196 tradingAcctBal, err := dcr.wallet.AccountBalance(dcr.ctx, 0, accts.TradingAccount) 1197 if err != nil { 1198 return nil, err 1199 } 1200 tradingAcctLocked, err := dcr.lockedAtoms(accts.TradingAccount) 1201 if err != nil { 1202 return nil, err 1203 } 1204 unmixedAcctBal, err := dcr.wallet.AccountBalance(dcr.ctx, 0, accts.UnmixedAccount) 1205 if err != nil { 1206 return nil, err 1207 } 1208 1209 bal.Available += toAtoms(tradingAcctBal.Spendable) - tradingAcctLocked 1210 bal.Immature += toAtoms(unmixedAcctBal.Total) 1211 bal.Locked += tradingAcctLocked 1212 1213 bal.Other[asset.BalanceCategoryUnmixed] = asset.CustomBalance{ 1214 Amount: toAtoms(unmixedAcctBal.Total), 1215 } 1216 1217 return bal, nil 1218 } 1219 1220 // Balance should return the total available funds in the wallet. 1221 func (dcr *ExchangeWallet) Balance() (*asset.Balance, error) { 1222 bal, err := dcr.balance() 1223 if err != nil { 1224 return nil, err 1225 } 1226 1227 reserves := dcr.bondReserves.Load() 1228 if reserves > bal.Available { // unmixed (immature) probably needs to trickle in 1229 dcr.log.Warnf("Available balance is below configured reserves: %f < %f", 1230 toDCR(bal.Available), toDCR(reserves)) 1231 bal.ReservesDeficit = reserves - bal.Available 1232 reserves = bal.Available 1233 } 1234 1235 bal.BondReserves = reserves 1236 bal.Available -= reserves 1237 bal.Locked += reserves 1238 1239 return bal, nil 1240 } 1241 1242 func bondsFeeBuffer(highFeeRate uint64) uint64 { 1243 const inputCount uint64 = 12 // plan for lots of inputs 1244 largeBondTxSize := dexdcr.MsgTxOverhead + dexdcr.P2SHOutputSize + 1 + dexdcr.BondPushDataSize + 1245 dexdcr.P2PKHOutputSize + inputCount*dexdcr.P2PKHInputSize 1246 // Normally we can plan on just 2 parallel "tracks" (single bond overlap 1247 // when bonds are expired and waiting to refund) but that may increase 1248 // temporarily if target tier is adjusted up. 1249 const parallelTracks uint64 = 4 1250 return parallelTracks * largeBondTxSize * highFeeRate 1251 } 1252 1253 // BondsFeeBuffer suggests how much extra may be required for the transaction 1254 // fees part of required bond reserves when bond rotation is enabled. 1255 func (dcr *ExchangeWallet) BondsFeeBuffer(feeRate uint64) uint64 { 1256 if feeRate == 0 { 1257 feeRate = dcr.targetFeeRateWithFallback(2, 0) 1258 } 1259 feeRate *= 2 // double the current live fee rate estimate 1260 return bondsFeeBuffer(feeRate) 1261 } 1262 1263 func (dcr *ExchangeWallet) SetBondReserves(reserves uint64) { 1264 dcr.bondReserves.Store(reserves) 1265 } 1266 1267 // FeeRate satisfies asset.FeeRater. 1268 func (dcr *ExchangeWallet) FeeRate() uint64 { 1269 const confTarget = 2 // 1 historically gives crazy rates 1270 rate, err := dcr.feeRate(confTarget) 1271 if err != nil && dcr.network != dex.Simnet { // log and return 0 1272 dcr.log.Errorf("feeRate error: %v", err) 1273 } 1274 return rate 1275 } 1276 1277 // feeRate returns the current optimal fee rate in atoms / byte. 1278 func (dcr *ExchangeWallet) feeRate(confTarget uint64) (uint64, error) { 1279 if dcr.ctx == nil { 1280 return 0, errors.New("not connected") 1281 } 1282 if feeEstimator, is := dcr.wallet.(FeeRateEstimator); is && !dcr.wallet.SpvMode() { 1283 dcrPerKB, err := feeEstimator.EstimateSmartFeeRate(dcr.ctx, int64(confTarget), chainjson.EstimateSmartFeeConservative) 1284 if err == nil && dcrPerKB > 0 { 1285 return dcrPerKBToAtomsPerByte(dcrPerKB) 1286 } 1287 if err != nil { 1288 dcr.log.Warnf("Failed to get local fee rate estimate: %v", err) 1289 } else { // dcrPerKB == 0 1290 dcr.log.Warnf("Local fee estimate is zero.") 1291 } 1292 } 1293 1294 cfg := dcr.config() 1295 1296 // Either SPV wallet or EstimateSmartFeeRate failed. 1297 if !cfg.apiFeeFallback { 1298 return 0, fmt.Errorf("fee rate estimation unavailable and external API is disabled") 1299 } 1300 1301 now := time.Now() 1302 1303 dcr.oracleFeesMtx.Lock() 1304 defer dcr.oracleFeesMtx.Unlock() 1305 oracleFee := dcr.oracleFees[confTarget] 1306 if now.Sub(oracleFee.stamp) < freshFeeAge { 1307 return oracleFee.rate, nil 1308 } 1309 if dcr.oracleFailing { 1310 return 0, errors.New("fee rate oracle is in a temporary failing state") 1311 } 1312 1313 dcr.log.Tracef("Retrieving fee rate from external fee oracle for %d target blocks", confTarget) 1314 dcrPerKB, err := fetchFeeFromOracle(dcr.ctx, dcr.network, confTarget) 1315 if err != nil { 1316 // Just log it and return zero. If we return an error, it's just logged 1317 // anyway, and we want to meter these logs. 1318 dcr.log.Meter("feeRate.fetch.fail", time.Hour).Errorf("external fee rate API failure: %v", err) 1319 // Flag the oracle as failing so subsequent requests don't also try and 1320 // fail after the request timeout. Remove the flag after a bit. 1321 dcr.oracleFailing = true 1322 time.AfterFunc(freshFeeAge, func() { 1323 dcr.oracleFeesMtx.Lock() 1324 dcr.oracleFailing = false 1325 dcr.oracleFeesMtx.Unlock() 1326 }) 1327 return 0, nil 1328 } 1329 if dcrPerKB <= 0 { 1330 return 0, fmt.Errorf("invalid fee rate %f from fee oracle", dcrPerKB) 1331 } 1332 // Convert to atoms/B and error if it is greater than fee rate limit. 1333 atomsPerByte, err := dcrPerKBToAtomsPerByte(dcrPerKB) 1334 if err != nil { 1335 return 0, err 1336 } 1337 if atomsPerByte > cfg.feeRateLimit { 1338 return 0, fmt.Errorf("fee rate from external API greater than fee rate limit: %v > %v", 1339 atomsPerByte, cfg.feeRateLimit) 1340 } 1341 dcr.oracleFees[confTarget] = feeStamped{atomsPerByte, now} 1342 return atomsPerByte, nil 1343 } 1344 1345 // dcrPerKBToAtomsPerByte converts a estimated feeRate from dcr/KB to atoms/B. 1346 func dcrPerKBToAtomsPerByte(dcrPerkB float64) (uint64, error) { 1347 // The caller should check for non-positive numbers, but don't allow 1348 // underflow when converting to an unsigned integer. 1349 if dcrPerkB < 0 { 1350 return 0, fmt.Errorf("negative fee rate") 1351 } 1352 // dcrPerkB * 1e8 / 1e3 => atomsPerB 1353 atomsPerKB, err := dcrutil.NewAmount(dcrPerkB) 1354 if err != nil { 1355 return 0, err 1356 } 1357 return uint64(dex.IntDivUp(int64(atomsPerKB), 1000)), nil 1358 } 1359 1360 // fetchFeeFromOracle gets the fee rate from the external API. 1361 func fetchFeeFromOracle(ctx context.Context, net dex.Network, nb uint64) (float64, error) { 1362 var uri string 1363 if net == dex.Testnet { 1364 uri = fmt.Sprintf("https://testnet.dcrdata.org/insight/api/utils/estimatefee?nbBlocks=%d", nb) 1365 } else { // mainnet and simnet 1366 uri = fmt.Sprintf("https://explorer.dcrdata.org/insight/api/utils/estimatefee?nbBlocks=%d", nb) 1367 } 1368 ctx, cancel := context.WithTimeout(ctx, 4*time.Second) 1369 defer cancel() 1370 var resp map[uint64]float64 1371 if err := dexnet.Get(ctx, uri, &resp); err != nil { 1372 return 0, err 1373 } 1374 if resp == nil { 1375 return 0, errors.New("null response") 1376 } 1377 dcrPerKB, ok := resp[nb] 1378 if !ok { 1379 return 0, errors.New("no fee rate for requested number of blocks") 1380 } 1381 return dcrPerKB, nil 1382 } 1383 1384 // targetFeeRateWithFallback attempts to get a fresh fee rate for the target 1385 // number of confirmations, but falls back to the suggestion or fallbackFeeRate 1386 // via feeRateWithFallback. 1387 func (dcr *ExchangeWallet) targetFeeRateWithFallback(confTarget, feeSuggestion uint64) uint64 { 1388 feeRate, err := dcr.feeRate(confTarget) 1389 if err != nil { 1390 dcr.log.Errorf("Failed to get fee rate: %v", err) 1391 } else if feeRate != 0 { 1392 dcr.log.Tracef("Obtained estimate for %d-conf fee rate, %d", confTarget, feeRate) 1393 return feeRate 1394 } 1395 1396 return dcr.feeRateWithFallback(feeSuggestion) 1397 } 1398 1399 // feeRateWithFallback filters the suggested fee rate by ensuring it is within 1400 // limits. If not, the configured fallbackFeeRate is returned and a warning 1401 // logged. 1402 func (dcr *ExchangeWallet) feeRateWithFallback(feeSuggestion uint64) uint64 { 1403 cfg := dcr.config() 1404 1405 if feeSuggestion > 0 && feeSuggestion < cfg.feeRateLimit { 1406 dcr.log.Tracef("Using caller's suggestion for fee rate, %d", feeSuggestion) 1407 return feeSuggestion 1408 } 1409 dcr.log.Warnf("No usable fee rate suggestion. Using fallback of %d", cfg.fallbackFeeRate) 1410 return cfg.fallbackFeeRate 1411 } 1412 1413 type amount uint64 1414 1415 func (a amount) String() string { 1416 return strconv.FormatFloat(dcrutil.Amount(a).ToCoin(), 'f', -1, 64) // dec, but no trailing zeros 1417 } 1418 1419 // MaxOrder generates information about the maximum order size and associated 1420 // fees that the wallet can support for the given DEX configuration. The 1421 // provided FeeSuggestion is used directly, and should be an estimate based on 1422 // current network conditions. For quote assets, the caller will have to 1423 // calculate lotSize based on a rate conversion from the base asset's lot size. 1424 // lotSize must not be zero and will cause a panic if so. 1425 func (dcr *ExchangeWallet) MaxOrder(ord *asset.MaxOrderForm) (*asset.SwapEstimate, error) { 1426 _, est, err := dcr.maxOrder(ord.LotSize, ord.FeeSuggestion, ord.MaxFeeRate) 1427 return est, err 1428 } 1429 1430 // maxOrder gets the estimate for MaxOrder, and also returns the 1431 // []*compositeUTXO and network fee rate to be used for further order estimation 1432 // without additional calls to listunspent. 1433 func (dcr *ExchangeWallet) maxOrder(lotSize, feeSuggestion, maxFeeRate uint64) (utxos []*compositeUTXO, est *asset.SwapEstimate, err error) { 1434 if lotSize == 0 { 1435 return nil, nil, errors.New("cannot divide by lotSize zero") 1436 } 1437 1438 utxos, err = dcr.spendableUTXOs() 1439 if err != nil { 1440 return nil, nil, fmt.Errorf("error parsing unspent outputs: %w", err) 1441 } 1442 avail := sumUTXOs(utxos) 1443 1444 // Start by attempting max lots with a basic fee. 1445 basicFee := dexdcr.InitTxSize * maxFeeRate 1446 // NOTE: Split tx is an order-time option. The max order is generally 1447 // attainable when split is used, regardless of whether they choose it on 1448 // the order form. Allow the split for max order purposes. 1449 trySplitTx := true 1450 1451 // Find the max lots we can fund. 1452 maxLotsInt := int(avail / (lotSize + basicFee)) 1453 oneLotTooMany := sort.Search(maxLotsInt+1, func(lots int) bool { 1454 _, _, _, err = dcr.estimateSwap(uint64(lots), lotSize, feeSuggestion, maxFeeRate, utxos, trySplitTx, 1.0) 1455 // The only failure mode of estimateSwap -> dcr.fund is when there is 1456 // not enough funds. 1457 return err != nil 1458 }) 1459 1460 maxLots := uint64(oneLotTooMany - 1) 1461 if oneLotTooMany == 0 { 1462 maxLots = 0 1463 } 1464 1465 if maxLots > 0 { 1466 est, _, _, err = dcr.estimateSwap(maxLots, lotSize, feeSuggestion, maxFeeRate, utxos, trySplitTx, 1.0) 1467 return utxos, est, err 1468 } 1469 1470 return nil, &asset.SwapEstimate{ 1471 FeeReservesPerLot: basicFee, 1472 }, nil 1473 } 1474 1475 // estimateSwap prepares an *asset.SwapEstimate. 1476 func (dcr *ExchangeWallet) estimateSwap(lots, lotSize, feeSuggestion, maxFeeRate uint64, utxos []*compositeUTXO, 1477 trySplit bool, feeBump float64) (*asset.SwapEstimate, bool /*split used*/, uint64 /* locked */, error) { 1478 // If there is a fee bump, the networkFeeRate can be higher than the 1479 // MaxFeeRate 1480 bumpedMaxRate := maxFeeRate 1481 bumpedNetRate := feeSuggestion 1482 if feeBump > 1 { 1483 bumpedMaxRate = uint64(math.Ceil(float64(bumpedMaxRate) * feeBump)) 1484 bumpedNetRate = uint64(math.Ceil(float64(bumpedNetRate) * feeBump)) 1485 } 1486 1487 feeReservesPerLot := bumpedMaxRate * dexdcr.InitTxSize 1488 1489 val := lots * lotSize 1490 // The orderEnough func does not account for a split transaction at the 1491 // start, so it is possible that funding for trySplit would actually choose 1492 // more UTXOs. Actual order funding accounts for this. For this estimate, we 1493 // will just not use a split tx if the split-adjusted required funds exceeds 1494 // the total value of the UTXO selected with this enough closure. 1495 sum, _, inputsSize, _, _, _, err := tryFund(utxos, orderEnough(val, lots, bumpedMaxRate, trySplit)) 1496 if err != nil { 1497 return nil, false, 0, err 1498 } 1499 1500 avail := sumUTXOs(utxos) 1501 reserves := dcr.bondReserves.Load() 1502 1503 digestInputs := func(inputsSize uint32) (reqFunds, maxFees, estHighFees, estLowFees uint64) { 1504 // NOTE: reqFunds = val + fees, so change (extra) will be sum-reqFunds 1505 reqFunds = calc.RequiredOrderFunds(val, uint64(inputsSize), lots, 1506 dexdcr.InitTxSizeBase, dexdcr.InitTxSize, bumpedMaxRate) // as in tryFund's enough func 1507 maxFees = reqFunds - val 1508 1509 estHighFunds := calc.RequiredOrderFunds(val, uint64(inputsSize), lots, 1510 dexdcr.InitTxSizeBase, dexdcr.InitTxSize, bumpedNetRate) 1511 estHighFees = estHighFunds - val 1512 1513 estLowFunds := calc.RequiredOrderFunds(val, uint64(inputsSize), 1, 1514 dexdcr.InitTxSizeBase, dexdcr.InitTxSize, bumpedNetRate) // best means single multi-lot match, even better than batch 1515 estLowFees = estLowFunds - val 1516 return 1517 } 1518 1519 reqFunds, maxFees, estHighFees, estLowFees := digestInputs(inputsSize) 1520 1521 // Math for split transactions is a little different. 1522 if trySplit { 1523 splitMaxFees := splitTxBaggage * bumpedMaxRate 1524 splitFees := splitTxBaggage * bumpedNetRate 1525 reqTotal := reqFunds + splitMaxFees // ~ rather than actually fund()ing again 1526 // We must consider splitMaxFees otherwise we'd skip the split on 1527 // account of excess baggage. 1528 if reqTotal <= sum && sum-reqTotal >= reserves { // avail-sum+extra > reserves 1529 return &asset.SwapEstimate{ 1530 Lots: lots, 1531 Value: val, 1532 MaxFees: maxFees + splitMaxFees, 1533 RealisticBestCase: estLowFees + splitFees, 1534 RealisticWorstCase: estHighFees + splitFees, 1535 FeeReservesPerLot: feeReservesPerLot, 1536 }, true, reqFunds, nil // requires reqTotal, but locks reqFunds in the split output 1537 } 1538 } 1539 1540 if sum > avail-reserves { // no split means no change available for reserves 1541 if trySplit { // if we already tried with a split, that's the best we can do 1542 return nil, false, 0, errors.New("balance too low to both fund order and maintain bond reserves") 1543 } 1544 // Like the fund() method, try with some utxos taken out of the mix for 1545 // reserves, as precise in value as possible. 1546 kept := leastOverFund(reserveEnough(reserves), utxos) 1547 utxos = utxoSetDiff(utxos, kept) 1548 sum, _, inputsSize, _, _, _, err = tryFund(utxos, orderEnough(val, lots, bumpedMaxRate, false)) 1549 if err != nil { // no joy with the reduced set 1550 return nil, false, 0, err 1551 } 1552 _, maxFees, estHighFees, estLowFees = digestInputs(inputsSize) 1553 } 1554 1555 // No split transaction. 1556 return &asset.SwapEstimate{ 1557 Lots: lots, 1558 Value: val, 1559 MaxFees: maxFees, 1560 RealisticBestCase: estLowFees, 1561 RealisticWorstCase: estHighFees, 1562 FeeReservesPerLot: feeReservesPerLot, 1563 }, false, sum, nil 1564 } 1565 1566 // PreSwap get order estimates based on the available funds and the wallet 1567 // configuration. 1568 func (dcr *ExchangeWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) { 1569 // Start with the maxOrder at the default configuration. This gets us the 1570 // utxo set, the network fee rate, and the wallet's maximum order size. 1571 // The utxo set can then be used repeatedly in estimateSwap at virtually 1572 // zero cost since there are no more RPC calls. 1573 // The utxo set is only used once right now, but when order-time options are 1574 // implemented, the utxos will be used to calculate option availability and 1575 // fees. 1576 utxos, maxEst, err := dcr.maxOrder(req.LotSize, req.FeeSuggestion, req.MaxFeeRate) 1577 if err != nil { 1578 return nil, err 1579 } 1580 if maxEst.Lots < req.Lots { // changing options isn't going to fix this, only lots 1581 return nil, fmt.Errorf("%d lots available for %d-lot order", maxEst.Lots, req.Lots) 1582 } 1583 1584 // Load the user's selected order-time options. 1585 customCfg := new(swapOptions) 1586 err = config.Unmapify(req.SelectedOptions, customCfg) 1587 if err != nil { 1588 return nil, fmt.Errorf("error parsing selected swap options: %w", err) 1589 } 1590 1591 // Parse the configured split transaction. 1592 cfg := dcr.config() 1593 split := cfg.useSplitTx 1594 if customCfg.Split != nil { 1595 split = *customCfg.Split 1596 } 1597 1598 // Parse the configured fee bump. 1599 bump, err := customCfg.feeBump() 1600 if err != nil { 1601 return nil, err 1602 } 1603 1604 // Get the estimate for the requested number of lots. 1605 est, _, _, err := dcr.estimateSwap(req.Lots, req.LotSize, req.FeeSuggestion, 1606 req.MaxFeeRate, utxos, split, bump) 1607 if err != nil { 1608 dcr.log.Warnf("estimateSwap failure: %v", err) 1609 } 1610 1611 // Always offer the split option, even for non-standing orders since 1612 // immediately spendable change many be desirable regardless. 1613 opts := []*asset.OrderOption{dcr.splitOption(req, utxos, bump)} 1614 1615 // Figure out what our maximum available fee bump is, within our 2x hard 1616 // limit. 1617 var maxBump float64 1618 var maxBumpEst *asset.SwapEstimate 1619 for maxBump = 2.0; maxBump > 1.01; maxBump -= 0.1 { 1620 if est == nil { 1621 break 1622 } 1623 tryEst, splitUsed, _, err := dcr.estimateSwap(req.Lots, req.LotSize, 1624 req.FeeSuggestion, req.MaxFeeRate, utxos, split, maxBump) 1625 // If the split used wasn't the configured value, this option is not 1626 // available. 1627 if err == nil && split == splitUsed { 1628 maxBumpEst = tryEst 1629 break 1630 } 1631 } 1632 1633 if maxBumpEst != nil { 1634 noBumpEst, _, _, err := dcr.estimateSwap(req.Lots, req.LotSize, req.FeeSuggestion, 1635 req.MaxFeeRate, utxos, split, 1.0) 1636 if err != nil { 1637 // shouldn't be possible, since we already succeeded with a higher bump. 1638 return nil, fmt.Errorf("error getting no-bump estimate: %w", err) 1639 } 1640 1641 bumpLabel := "2X" 1642 if maxBump < 2.0 { 1643 bumpLabel = strconv.FormatFloat(maxBump, 'f', 1, 64) + "X" 1644 } 1645 1646 extraFees := maxBumpEst.RealisticWorstCase - noBumpEst.RealisticWorstCase 1647 desc := fmt.Sprintf("Add a fee multiplier up to %.1fx (up to ~%s DCR more) for faster settlement when network traffic is high.", 1648 maxBump, amount(extraFees)) 1649 1650 opts = append(opts, &asset.OrderOption{ 1651 ConfigOption: asset.ConfigOption{ 1652 Key: swapFeeBumpKey, 1653 DisplayName: "Faster Swaps", 1654 Description: desc, 1655 DefaultValue: 1.0, 1656 }, 1657 XYRange: &asset.XYRange{ 1658 Start: asset.XYRangePoint{ 1659 Label: "1X", 1660 X: 1.0, 1661 Y: float64(req.FeeSuggestion), 1662 }, 1663 End: asset.XYRangePoint{ 1664 Label: bumpLabel, 1665 X: maxBump, 1666 Y: float64(req.FeeSuggestion) * maxBump, 1667 }, 1668 XUnit: "X", 1669 YUnit: "atoms/B", 1670 }, 1671 }) 1672 } 1673 1674 return &asset.PreSwap{ 1675 Estimate: est, // may be nil so we can present options, which in turn affect estimate feasibility 1676 Options: opts, 1677 }, nil 1678 } 1679 1680 // SingleLotSwapRefundFees returns the fees for a swap and refund transaction 1681 // for a single lot. 1682 func (dcr *ExchangeWallet) SingleLotSwapRefundFees(_ uint32, feeSuggestion uint64, useSafeTxSize bool) (swapFees uint64, refundFees uint64, err error) { 1683 var numInputs uint64 1684 if useSafeTxSize { 1685 numInputs = 12 1686 } else { 1687 numInputs = 2 1688 } 1689 swapTxSize := dexdcr.InitTxSizeBase + (numInputs * dexdcr.P2PKHInputSize) 1690 refundTxSize := dexdcr.MsgTxOverhead + dexdcr.TxInOverhead + dexdcr.RefundSigScriptSize + dexdcr.P2PKHOutputSize 1691 return swapTxSize * feeSuggestion, uint64(refundTxSize) * feeSuggestion, nil 1692 } 1693 1694 // MaxFundingFees returns the maximum funding fees for an order/multi-order. 1695 func (dcr *ExchangeWallet) MaxFundingFees(numTrades uint32, feeRate uint64, options map[string]string) uint64 { 1696 customCfg, err := decodeFundMultiOptions(options) 1697 if err != nil { 1698 dcr.log.Errorf("Error decoding multi-fund settings: %v", err) 1699 return 0 1700 } 1701 if !customCfg.Split { 1702 return 0 1703 } 1704 1705 const numInputs = 12 // plan for lots of inputs to get a safe estimate 1706 splitTxSize := dexdcr.MsgTxOverhead + (numInputs * dexdcr.P2PKHInputSize) + (uint64(numTrades+1) * dexdcr.P2PKHOutputSize) 1707 return splitTxSize * dcr.config().feeRateLimit 1708 } 1709 1710 // splitOption constructs an *asset.OrderOption with customized text based on the 1711 // difference in fees between the configured and test split condition. 1712 func (dcr *ExchangeWallet) splitOption(req *asset.PreSwapForm, utxos []*compositeUTXO, bump float64) *asset.OrderOption { 1713 opt := &asset.OrderOption{ 1714 ConfigOption: asset.ConfigOption{ 1715 Key: splitKey, 1716 DisplayName: "Pre-size Funds", 1717 IsBoolean: true, 1718 DefaultValue: dcr.config().useSplitTx, // not nil interface 1719 ShowByDefault: true, 1720 }, 1721 Boolean: &asset.BooleanConfig{}, 1722 } 1723 1724 noSplitEst, _, noSplitLocked, err := dcr.estimateSwap(req.Lots, req.LotSize, 1725 req.FeeSuggestion, req.MaxFeeRate, utxos, false, bump) 1726 if err != nil { 1727 dcr.log.Errorf("estimateSwap (no split) error: %v", err) 1728 opt.Boolean.Reason = fmt.Sprintf("estimate without a split failed with \"%v\"", err) 1729 return opt // utility and overlock report unavailable, but show the option 1730 } 1731 splitEst, splitUsed, splitLocked, err := dcr.estimateSwap(req.Lots, req.LotSize, 1732 req.FeeSuggestion, req.MaxFeeRate, utxos, true, bump) 1733 if err != nil { 1734 dcr.log.Errorf("estimateSwap (with split) error: %v", err) 1735 opt.Boolean.Reason = fmt.Sprintf("estimate with a split failed with \"%v\"", err) 1736 return opt // utility and overlock report unavailable, but show the option 1737 } 1738 1739 if !splitUsed || splitLocked >= noSplitLocked { // locked check should be redundant 1740 opt.Boolean.Reason = "avoids no DCR overlock for this order (ignored)" 1741 opt.Description = "A split transaction for this order avoids no DCR overlock, but adds additional fees." 1742 opt.DefaultValue = false 1743 return opt // not enabled by default, but explain why 1744 } 1745 1746 overlock := noSplitLocked - splitLocked 1747 pctChange := (float64(splitEst.RealisticWorstCase)/float64(noSplitEst.RealisticWorstCase) - 1) * 100 1748 if pctChange > 1 { 1749 opt.Boolean.Reason = fmt.Sprintf("+%d%% fees, avoids %s DCR overlock", int(math.Round(pctChange)), amount(overlock)) 1750 } else { 1751 opt.Boolean.Reason = fmt.Sprintf("+%.1f%% fees, avoids %s DCR overlock", pctChange, amount(overlock)) 1752 } 1753 1754 xtraFees := splitEst.RealisticWorstCase - noSplitEst.RealisticWorstCase 1755 opt.Description = fmt.Sprintf("Using a split transaction may prevent temporary overlock of %s DCR, but for additional fees of %s DCR", 1756 amount(overlock), amount(xtraFees)) 1757 1758 return opt 1759 } 1760 1761 func (dcr *ExchangeWallet) preRedeem(numLots, feeSuggestion uint64, options map[string]string) (*asset.PreRedeem, error) { 1762 cfg := dcr.config() 1763 1764 feeRate := feeSuggestion 1765 if feeRate == 0 { // or just document that the caller must set it? 1766 feeRate = dcr.targetFeeRateWithFallback(cfg.redeemConfTarget, feeSuggestion) 1767 } 1768 // Best is one transaction with req.Lots inputs and 1 output. 1769 var best uint64 = dexdcr.MsgTxOverhead 1770 // Worst is req.Lots transactions, each with one input and one output. 1771 var worst uint64 = dexdcr.MsgTxOverhead * numLots 1772 var inputSize uint64 = dexdcr.TxInOverhead + dexdcr.RedeemSwapSigScriptSize 1773 var outputSize uint64 = dexdcr.P2PKHOutputSize 1774 best += inputSize*numLots + outputSize 1775 worst += (inputSize + outputSize) * numLots 1776 1777 // Read the order options. 1778 customCfg := new(redeemOptions) 1779 err := config.Unmapify(options, customCfg) 1780 if err != nil { 1781 return nil, fmt.Errorf("error parsing selected options: %w", err) 1782 } 1783 1784 // Parse the configured fee bump. 1785 var currentBump float64 = 1.0 1786 if customCfg.FeeBump != nil { 1787 bump := *customCfg.FeeBump 1788 if bump < 1.0 || bump > 2.0 { 1789 return nil, fmt.Errorf("invalid fee bump: %f", bump) 1790 } 1791 currentBump = bump 1792 } 1793 1794 opts := []*asset.OrderOption{{ 1795 ConfigOption: asset.ConfigOption{ 1796 Key: redeemFeeBumpFee, 1797 DisplayName: "Faster Redemption", 1798 Description: "Bump the redemption transaction fees up to 2x for faster confirmations on your redemption transaction.", 1799 DefaultValue: 1.0, 1800 }, 1801 XYRange: &asset.XYRange{ 1802 Start: asset.XYRangePoint{ 1803 Label: "1X", 1804 X: 1.0, 1805 Y: float64(feeRate), 1806 }, 1807 End: asset.XYRangePoint{ 1808 Label: "2X", 1809 X: 2.0, 1810 Y: float64(feeRate * 2), 1811 }, 1812 YUnit: "atoms/B", 1813 XUnit: "X", 1814 }, 1815 }} 1816 1817 return &asset.PreRedeem{ 1818 Estimate: &asset.RedeemEstimate{ 1819 RealisticWorstCase: uint64(math.Round(float64(worst*feeRate) * currentBump)), 1820 RealisticBestCase: uint64(math.Round(float64(best*feeRate) * currentBump)), 1821 }, 1822 Options: opts, 1823 }, nil 1824 } 1825 1826 // PreRedeem generates an estimate of the range of redemption fees that could 1827 // be assessed. 1828 func (dcr *ExchangeWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, error) { 1829 return dcr.preRedeem(req.Lots, req.FeeSuggestion, req.SelectedOptions) 1830 } 1831 1832 // SingleLotRedeemFees returns the fees for a redeem transaction for a single lot. 1833 func (dcr *ExchangeWallet) SingleLotRedeemFees(_ uint32, feeSuggestion uint64) (uint64, error) { 1834 preRedeem, err := dcr.preRedeem(1, feeSuggestion, nil) 1835 if err != nil { 1836 return 0, err 1837 } 1838 1839 dcr.log.Tracef("SingleLotRedeemFees: worst case = %d, feeSuggestion = %d", preRedeem.Estimate.RealisticWorstCase, feeSuggestion) 1840 1841 return preRedeem.Estimate.RealisticWorstCase, nil 1842 } 1843 1844 // FundOrder selects coins for use in an order. The coins will be locked, and 1845 // will not be returned in subsequent calls to FundOrder or calculated in calls 1846 // to Available, unless they are unlocked with ReturnCoins. 1847 // The returned []dex.Bytes contains the redeem scripts for the selected coins. 1848 // Equal number of coins and redeemed scripts must be returned. A nil or empty 1849 // dex.Bytes should be appended to the redeem scripts collection for coins with 1850 // no redeem script. 1851 func (dcr *ExchangeWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uint64, error) { 1852 cfg := dcr.config() 1853 1854 // Consumer checks dex asset version, so maybe this is not our job: 1855 // if ord.DEXConfig.Version != dcr.Info().Version { 1856 // return nil, nil, fmt.Errorf("asset version mismatch: server = %d, client = %d", 1857 // ord.DEXConfig.Version, dcr.Info().Version) 1858 // } 1859 if ord.Value == 0 { 1860 return nil, nil, 0, fmt.Errorf("cannot fund value = 0") 1861 } 1862 if ord.MaxSwapCount == 0 { 1863 return nil, nil, 0, fmt.Errorf("cannot fund a zero-lot order") 1864 } 1865 if ord.FeeSuggestion > ord.MaxFeeRate { 1866 return nil, nil, 0, fmt.Errorf("fee suggestion %d > max fee rate %d", ord.FeeSuggestion, ord.MaxFeeRate) 1867 } 1868 if ord.FeeSuggestion > cfg.feeRateLimit { 1869 return nil, nil, 0, fmt.Errorf("suggested fee > configured limit. %d > %d", ord.FeeSuggestion, cfg.feeRateLimit) 1870 } 1871 // Check wallet's fee rate limit against server's max fee rate 1872 if cfg.feeRateLimit < ord.MaxFeeRate { 1873 return nil, nil, 0, fmt.Errorf( 1874 "%v: server's max fee rate %v higher than configured fee rate limit %v", 1875 dex.BipIDSymbol(BipID), ord.MaxFeeRate, cfg.feeRateLimit) 1876 } 1877 1878 customCfg := new(swapOptions) 1879 err := config.Unmapify(ord.Options, customCfg) 1880 if err != nil { 1881 return nil, nil, 0, fmt.Errorf("Error parsing swap options") 1882 } 1883 1884 // Check ord.Options for a FeeBump here 1885 bumpedMaxRate, err := calcBumpedRate(ord.MaxFeeRate, customCfg.FeeBump) 1886 if err != nil { 1887 dcr.log.Errorf("calcBumpRate error: %v", err) 1888 } 1889 1890 // If a split is not requested, but is forced, create an extra output from 1891 // the split tx to help avoid a forced split in subsequent orders. 1892 var extraSplitOutput uint64 1893 useSplit := cfg.useSplitTx 1894 if customCfg.Split != nil { 1895 useSplit = *customCfg.Split 1896 } 1897 1898 changeForReserves := useSplit && dcr.wallet.Accounts().UnmixedAccount == "" 1899 reserves := dcr.bondReserves.Load() 1900 coins, redeemScripts, sum, inputsSize, err := dcr.fund(reserves, 1901 orderEnough(ord.Value, ord.MaxSwapCount, bumpedMaxRate, changeForReserves)) 1902 if err != nil { 1903 if !changeForReserves && reserves > 0 { // split not selected, or it's a mixing account where change isn't usable 1904 // Force a split if funding failure may be due to reserves. 1905 dcr.log.Infof("Retrying order funding with a forced split transaction to help respect reserves.") 1906 useSplit = true 1907 keepForSplitToo := reserves + (bumpedMaxRate * dexdcr.P2PKHInputSize) // so we fail before split() if it's really that tight 1908 coins, redeemScripts, sum, inputsSize, err = dcr.fund(keepForSplitToo, 1909 orderEnough(ord.Value, ord.MaxSwapCount, bumpedMaxRate, useSplit)) 1910 // And make an extra output for the reserves amount plus additional 1911 // fee buffer (double) to help avoid this for a while in the future. 1912 // This also deals with mixing wallets not having usable change. 1913 extraSplitOutput = reserves + bondsFeeBuffer(cfg.feeRateLimit) 1914 } 1915 if err != nil { 1916 return nil, nil, 0, fmt.Errorf("error funding order value of %s DCR: %w", 1917 amount(ord.Value), err) 1918 } 1919 } 1920 1921 // Send a split, if preferred or required. 1922 if useSplit { 1923 // We apply the bumped fee rate to the split transaction when the 1924 // PreSwap is created, so we use that bumped rate here too. 1925 // But first, check that it's within bounds. 1926 rawFeeRate := ord.FeeSuggestion 1927 if rawFeeRate == 0 { 1928 // TODO 1929 // 1.0: Error when no suggestion. 1930 // return nil, false, fmt.Errorf("cannot do a split transaction without a fee rate suggestion from the server") 1931 rawFeeRate = dcr.targetFeeRateWithFallback(cfg.redeemConfTarget, 0) 1932 // We PreOrder checked this as <= MaxFeeRate, so use that as an 1933 // upper limit. 1934 if rawFeeRate > ord.MaxFeeRate { 1935 rawFeeRate = ord.MaxFeeRate 1936 } 1937 } 1938 splitFeeRate, err := calcBumpedRate(rawFeeRate, customCfg.FeeBump) 1939 if err != nil { 1940 dcr.log.Errorf("calcBumpRate error: %v", err) 1941 } 1942 1943 splitCoins, split, fees, err := dcr.split(ord.Value, ord.MaxSwapCount, coins, 1944 inputsSize, splitFeeRate, bumpedMaxRate, extraSplitOutput) 1945 if err != nil { // potentially try again with extraSplitOutput=0 if it wasn't already 1946 if _, errRet := dcr.returnCoins(coins); errRet != nil { 1947 dcr.log.Warnf("Failed to unlock funding coins %v: %v", coins, errRet) 1948 } 1949 return nil, nil, 0, err 1950 } 1951 if split { 1952 return splitCoins, []dex.Bytes{nil}, fees, nil // no redeem script required for split tx output 1953 } 1954 return splitCoins, redeemScripts, 0, nil // splitCoins == coins 1955 } 1956 1957 dcr.log.Infof("Funding %s DCR order with coins %v worth %s", amount(ord.Value), coins, amount(sum)) 1958 1959 return coins, redeemScripts, 0, nil 1960 } 1961 1962 // fundMultiOptions are the possible order options when calling FundMultiOrder. 1963 type fundMultiOptions struct { 1964 // Split, if true, and multi-order cannot be funded with the existing UTXOs 1965 // in the wallet without going over the maxLock limit, a split transaction 1966 // will be created with one output per order. 1967 // 1968 // Use the multiSplitKey const defined above in the options map to set this option. 1969 Split bool `ini:"multisplit"` 1970 // SplitBuffer, if set, will instruct the wallet to add a buffer onto each 1971 // output of the multi-order split transaction (if the split is needed). 1972 // SplitBuffer is defined as a percentage of the output. If a .1 BTC output 1973 // is required for an order and SplitBuffer is set to 5, a .105 BTC output 1974 // will be created. 1975 // 1976 // The motivation for this is to assist market makers in having to do the 1977 // least amount of splits as possible. It is useful when DCR is the quote 1978 // asset on a market, and the price is increasing. During a market maker's 1979 // operation, it will frequently have to cancel and replace orders as the 1980 // rate moves. If BTC is the quote asset on a market, and the rate has 1981 // lightly increased, the market maker will need to lock slightly more of 1982 // the quote asset for the same amount of lots of the base asset. If there 1983 // is no split buffer, this may necessitate a new split transaction. 1984 // 1985 // Use the multiSplitBufferKey const defined above in the options map to set this. 1986 SplitBuffer float64 `ini:"multisplitbuffer"` 1987 } 1988 1989 func decodeFundMultiOptions(options map[string]string) (*fundMultiOptions, error) { 1990 opts := new(fundMultiOptions) 1991 return opts, config.Unmapify(options, opts) 1992 } 1993 1994 // orderWithLeastOverFund returns the index of the order from a slice of orders 1995 // that requires the least over-funding without using more than maxLock. It 1996 // also returns the UTXOs that were used to fund the order. If none can be 1997 // funded without using more than maxLock, -1 is returned. 1998 func (dcr *ExchangeWallet) orderWithLeastOverFund(maxLock, feeRate uint64, orders []*asset.MultiOrderValue, utxos []*compositeUTXO) (orderIndex int, leastOverFundingUTXOs []*compositeUTXO) { 1999 minOverFund := uint64(math.MaxUint64) 2000 orderIndex = -1 2001 for i, value := range orders { 2002 enough := orderEnough(value.Value, value.MaxSwapCount, feeRate, false) 2003 var fundingUTXOs []*compositeUTXO 2004 if maxLock > 0 { 2005 fundingUTXOs = leastOverFundWithLimit(enough, maxLock, utxos) 2006 } else { 2007 fundingUTXOs = leastOverFund(enough, utxos) 2008 } 2009 if len(fundingUTXOs) == 0 { 2010 continue 2011 } 2012 sum := sumUTXOs(fundingUTXOs) 2013 overFund := sum - value.Value 2014 if overFund < minOverFund { 2015 minOverFund = overFund 2016 orderIndex = i 2017 leastOverFundingUTXOs = fundingUTXOs 2018 } 2019 } 2020 return 2021 } 2022 2023 // fundsRequiredForMultiOrders returns an slice of the required funds for each 2024 // of a slice of orders and the total required funds. 2025 func (dcr *ExchangeWallet) fundsRequiredForMultiOrders(orders []*asset.MultiOrderValue, feeRate uint64, splitBuffer float64) ([]uint64, uint64) { 2026 requiredForOrders := make([]uint64, len(orders)) 2027 var totalRequired uint64 2028 2029 for i, value := range orders { 2030 req := calc.RequiredOrderFunds(value.Value, dexdcr.P2PKHInputSize, value.MaxSwapCount, 2031 dexdcr.InitTxSizeBase, dexdcr.InitTxSize, feeRate) 2032 req = uint64(math.Round(float64(req) * (100 + splitBuffer) / 100)) 2033 requiredForOrders[i] = req 2034 totalRequired += req 2035 } 2036 2037 return requiredForOrders, totalRequired 2038 } 2039 2040 // fundMultiBestEffort makes a best effort to fund every order. If it is not 2041 // possible, it returns coins for the orders that could be funded. The coins 2042 // that fund each order are returned in the same order as the values that were 2043 // passed in. If a split is allowed and all orders cannot be funded, nil slices 2044 // are returned. 2045 func (dcr *ExchangeWallet) fundMultiBestEffort(keep, maxLock uint64, values []*asset.MultiOrderValue, 2046 maxFeeRate uint64, splitAllowed bool) ([]asset.Coins, [][]dex.Bytes, []*fundingCoin, error) { 2047 utxos, err := dcr.spendableUTXOs() 2048 if err != nil { 2049 return nil, nil, nil, fmt.Errorf("error getting spendable utxos: %w", err) 2050 } 2051 2052 var avail uint64 2053 for _, utxo := range utxos { 2054 avail += toAtoms(utxo.rpc.Amount) 2055 } 2056 2057 fundAllOrders := func() [][]*compositeUTXO { 2058 indexToFundingCoins := make(map[int][]*compositeUTXO, len(values)) 2059 remainingUTXOs := utxos 2060 remainingOrders := values 2061 remainingIndexes := make([]int, len(values)) 2062 for i := range remainingIndexes { 2063 remainingIndexes[i] = i 2064 } 2065 var totalFunded uint64 2066 for range values { 2067 orderIndex, fundingUTXOs := dcr.orderWithLeastOverFund(maxLock-totalFunded, maxFeeRate, remainingOrders, remainingUTXOs) 2068 if orderIndex == -1 { 2069 return nil 2070 } 2071 totalFunded += sumUTXOs(fundingUTXOs) 2072 if totalFunded > avail-keep { 2073 return nil 2074 } 2075 newRemainingOrders := make([]*asset.MultiOrderValue, 0, len(remainingOrders)-1) 2076 newRemainingIndexes := make([]int, 0, len(remainingOrders)-1) 2077 for j := range remainingOrders { 2078 if j != orderIndex { 2079 newRemainingOrders = append(newRemainingOrders, remainingOrders[j]) 2080 newRemainingIndexes = append(newRemainingIndexes, remainingIndexes[j]) 2081 } 2082 } 2083 indexToFundingCoins[remainingIndexes[orderIndex]] = fundingUTXOs 2084 remainingOrders = newRemainingOrders 2085 remainingIndexes = newRemainingIndexes 2086 remainingUTXOs = utxoSetDiff(remainingUTXOs, fundingUTXOs) 2087 } 2088 allFundingUTXOs := make([][]*compositeUTXO, len(values)) 2089 for i := range values { 2090 allFundingUTXOs[i] = indexToFundingCoins[i] 2091 } 2092 return allFundingUTXOs 2093 } 2094 2095 fundInOrder := func(orderedValues []*asset.MultiOrderValue) [][]*compositeUTXO { 2096 allFundingUTXOs := make([][]*compositeUTXO, 0, len(orderedValues)) 2097 remainingUTXOs := utxos 2098 var totalFunded uint64 2099 for _, value := range orderedValues { 2100 enough := orderEnough(value.Value, value.MaxSwapCount, maxFeeRate, false) 2101 2102 var fundingUTXOs []*compositeUTXO 2103 if maxLock > 0 { 2104 if maxLock < totalFunded { 2105 // Should never happen unless there is a bug in leastOverFundWithLimit 2106 dcr.log.Errorf("maxLock < totalFunded. %d < %d", maxLock, totalFunded) 2107 return allFundingUTXOs 2108 } 2109 fundingUTXOs = leastOverFundWithLimit(enough, maxLock-totalFunded, remainingUTXOs) 2110 } else { 2111 fundingUTXOs = leastOverFund(enough, remainingUTXOs) 2112 } 2113 if len(fundingUTXOs) == 0 { 2114 return allFundingUTXOs 2115 } 2116 totalFunded += sumUTXOs(fundingUTXOs) 2117 if totalFunded > avail-keep { 2118 return allFundingUTXOs 2119 } 2120 allFundingUTXOs = append(allFundingUTXOs, fundingUTXOs) 2121 remainingUTXOs = utxoSetDiff(remainingUTXOs, fundingUTXOs) 2122 } 2123 return allFundingUTXOs 2124 } 2125 2126 returnValues := func(allFundingUTXOs [][]*compositeUTXO) (coins []asset.Coins, redeemScripts [][]dex.Bytes, fundingCoins []*fundingCoin, err error) { 2127 coins = make([]asset.Coins, len(allFundingUTXOs)) 2128 fundingCoins = make([]*fundingCoin, 0, len(allFundingUTXOs)) 2129 redeemScripts = make([][]dex.Bytes, len(allFundingUTXOs)) 2130 for i, fundingUTXOs := range allFundingUTXOs { 2131 coins[i] = make(asset.Coins, len(fundingUTXOs)) 2132 redeemScripts[i] = make([]dex.Bytes, len(fundingUTXOs)) 2133 for j, output := range fundingUTXOs { 2134 txHash, err := chainhash.NewHashFromStr(output.rpc.TxID) 2135 if err != nil { 2136 return nil, nil, nil, fmt.Errorf("error decoding txid: %w", err) 2137 } 2138 coins[i][j] = newOutput(txHash, output.rpc.Vout, toAtoms(output.rpc.Amount), output.rpc.Tree) 2139 fundingCoins = append(fundingCoins, &fundingCoin{ 2140 op: newOutput(txHash, output.rpc.Vout, toAtoms(output.rpc.Amount), output.rpc.Tree), 2141 addr: output.rpc.Address, 2142 }) 2143 redeemScript, err := hex.DecodeString(output.rpc.RedeemScript) 2144 if err != nil { 2145 return nil, nil, nil, fmt.Errorf("error decoding redeem script for %s, script = %s: %w", 2146 txHash, output.rpc.RedeemScript, err) 2147 } 2148 redeemScripts[i][j] = redeemScript 2149 } 2150 } 2151 return 2152 } 2153 2154 // Attempt to fund all orders by selecting the order that requires the least 2155 // over funding, removing the funding utxos from the set of available utxos, 2156 // and continuing until all orders are funded. 2157 allFundingUTXOs := fundAllOrders() 2158 if allFundingUTXOs != nil { 2159 return returnValues(allFundingUTXOs) 2160 } 2161 2162 // Return nil if a split is allowed. There is no need to fund in priority 2163 // order if a split will be done regardless. 2164 if splitAllowed { 2165 return returnValues([][]*compositeUTXO{}) 2166 } 2167 2168 // If could not fully fund, fund as much as possible in the priority 2169 // order. 2170 allFundingUTXOs = fundInOrder(values) 2171 return returnValues(allFundingUTXOs) 2172 } 2173 2174 // fundMultiSplitTx uses the utxos provided and attempts to fund a multi-split 2175 // transaction to fund each of the orders. If successful, it returns the 2176 // funding coins and outputs. 2177 func (dcr *ExchangeWallet) fundMultiSplitTx(orders []*asset.MultiOrderValue, utxos []*compositeUTXO, 2178 splitTxFeeRate, maxFeeRate uint64, splitBuffer float64, keep, maxLock uint64) (bool, asset.Coins, []*fundingCoin) { 2179 _, totalOutputRequired := dcr.fundsRequiredForMultiOrders(orders, maxFeeRate, splitBuffer) 2180 2181 var splitTxSizeWithoutInputs uint32 = dexdcr.MsgTxOverhead 2182 numOutputs := len(orders) 2183 if keep > 0 { 2184 numOutputs++ 2185 } 2186 splitTxSizeWithoutInputs += uint32(dexdcr.P2PKHOutputSize * numOutputs) 2187 2188 enough := func(sum uint64, size uint32, utxo *compositeUTXO) (bool, uint64) { 2189 totalSum := sum + toAtoms(utxo.rpc.Amount) 2190 totalSize := size + utxo.input.Size() 2191 splitTxFee := uint64(splitTxSizeWithoutInputs+totalSize) * splitTxFeeRate 2192 req := totalOutputRequired + splitTxFee 2193 return totalSum >= req, totalSum - req 2194 } 2195 2196 var avail uint64 2197 for _, utxo := range utxos { 2198 avail += toAtoms(utxo.rpc.Amount) 2199 } 2200 2201 fundSplitCoins, _, spents, _, inputsSize, err := dcr.fundInternalWithUTXOs(utxos, keep, enough, false) 2202 if err != nil { 2203 return false, nil, nil 2204 } 2205 2206 if maxLock > 0 { 2207 totalSize := inputsSize + uint64(splitTxSizeWithoutInputs) 2208 if totalOutputRequired+(totalSize*splitTxFeeRate) > maxLock { 2209 return false, nil, nil 2210 } 2211 } 2212 2213 return true, fundSplitCoins, spents 2214 } 2215 2216 // submitMultiSplitTx creates a multi-split transaction using fundingCoins with 2217 // one output for each order, and submits it to the network. 2218 func (dcr *ExchangeWallet) submitMultiSplitTx(fundingCoins asset.Coins, _ /* spents */ []*fundingCoin, orders []*asset.MultiOrderValue, 2219 maxFeeRate, splitTxFeeRate uint64, splitBuffer float64) ([]asset.Coins, uint64, error) { 2220 baseTx := wire.NewMsgTx() 2221 _, err := dcr.addInputCoins(baseTx, fundingCoins) 2222 if err != nil { 2223 return nil, 0, err 2224 } 2225 2226 accts := dcr.wallet.Accounts() 2227 getAddr := func() (stdaddr.Address, error) { 2228 if accts.TradingAccount != "" { 2229 return dcr.wallet.ExternalAddress(dcr.ctx, accts.TradingAccount) 2230 } 2231 return dcr.wallet.ExternalAddress(dcr.ctx, accts.PrimaryAccount) 2232 } 2233 2234 requiredForOrders, _ := dcr.fundsRequiredForMultiOrders(orders, maxFeeRate, splitBuffer) 2235 outputAddresses := make([]stdaddr.Address, len(orders)) 2236 for i, req := range requiredForOrders { 2237 outputAddr, err := getAddr() 2238 if err != nil { 2239 return nil, 0, err 2240 } 2241 outputAddresses[i] = outputAddr 2242 payScriptVer, payScript := outputAddr.PaymentScript() 2243 txOut := newTxOut(int64(req), payScriptVer, payScript) 2244 baseTx.AddTxOut(txOut) 2245 } 2246 2247 tx, err := dcr.sendWithReturn(baseTx, splitTxFeeRate, -1) 2248 if err != nil { 2249 return nil, 0, err 2250 } 2251 2252 coins := make([]asset.Coins, len(orders)) 2253 fcs := make([]*fundingCoin, len(orders)) 2254 for i := range coins { 2255 coins[i] = asset.Coins{newOutput(tx.CachedTxHash(), uint32(i), uint64(tx.TxOut[i].Value), wire.TxTreeRegular)} 2256 fcs[i] = &fundingCoin{ 2257 op: newOutput(tx.CachedTxHash(), uint32(i), uint64(tx.TxOut[i].Value), wire.TxTreeRegular), 2258 addr: outputAddresses[i].String(), 2259 } 2260 } 2261 dcr.lockFundingCoins(fcs) 2262 2263 var totalOut uint64 2264 for _, txOut := range tx.TxOut { 2265 totalOut += uint64(txOut.Value) 2266 } 2267 2268 var totalIn uint64 2269 for _, txIn := range fundingCoins { 2270 totalIn += txIn.Value() 2271 } 2272 2273 txHash := tx.CachedTxHash() 2274 dcr.addTxToHistory(&asset.WalletTransaction{ 2275 Type: asset.Split, 2276 ID: txHash.String(), 2277 Fees: totalIn - totalOut, 2278 }, txHash, true) 2279 2280 return coins, totalIn - totalOut, nil 2281 } 2282 2283 // fundMultiWithSplit creates a split transaction to fund multiple orders. It 2284 // attempts to fund as many of the orders as possible without a split transaction, 2285 // and only creates a split transaction for the remaining orders. This is only 2286 // called after it has been determined that all of the orders cannot be funded 2287 // without a split transaction. 2288 func (dcr *ExchangeWallet) fundMultiWithSplit(keep, maxLock uint64, values []*asset.MultiOrderValue, 2289 splitTxFeeRate, maxFeeRate uint64, splitBuffer float64) ([]asset.Coins, [][]dex.Bytes, uint64, error) { 2290 utxos, err := dcr.spendableUTXOs() 2291 if err != nil { 2292 return nil, nil, 0, fmt.Errorf("error getting spendable utxos: %w", err) 2293 } 2294 2295 var avail uint64 2296 for _, utxo := range utxos { 2297 avail += toAtoms(utxo.rpc.Amount) 2298 } 2299 2300 canFund, splitCoins, splitSpents := dcr.fundMultiSplitTx(values, utxos, splitTxFeeRate, maxFeeRate, splitBuffer, keep, maxLock) 2301 if !canFund { 2302 return nil, nil, 0, fmt.Errorf("cannot fund all with split") 2303 } 2304 2305 remainingUTXOs := utxos 2306 remainingOrders := values 2307 2308 // The return values must be in the same order as the values that were 2309 // passed in, so we keep track of the original indexes here. 2310 indexToFundingCoins := make(map[int][]*compositeUTXO, len(values)) 2311 remainingIndexes := make([]int, len(values)) 2312 for i := range remainingIndexes { 2313 remainingIndexes[i] = i 2314 } 2315 2316 var totalFunded uint64 2317 2318 // Find each of the orders that can be funded without being included 2319 // in the split transaction. 2320 for range values { 2321 // First find the order the can be funded with the least overlock. 2322 // If there is no order that can be funded without going over the 2323 // maxLock limit, or not leaving enough for bond reserves, then all 2324 // of the remaining orders must be funded with the split transaction. 2325 orderIndex, fundingUTXOs := dcr.orderWithLeastOverFund(maxLock-totalFunded, maxFeeRate, remainingOrders, remainingUTXOs) 2326 if orderIndex == -1 { 2327 break 2328 } 2329 totalFunded += sumUTXOs(fundingUTXOs) 2330 if totalFunded > avail-keep { 2331 break 2332 } 2333 2334 newRemainingOrders := make([]*asset.MultiOrderValue, 0, len(remainingOrders)-1) 2335 newRemainingIndexes := make([]int, 0, len(remainingOrders)-1) 2336 for j := range remainingOrders { 2337 if j != orderIndex { 2338 newRemainingOrders = append(newRemainingOrders, remainingOrders[j]) 2339 newRemainingIndexes = append(newRemainingIndexes, remainingIndexes[j]) 2340 } 2341 } 2342 remainingUTXOs = utxoSetDiff(remainingUTXOs, fundingUTXOs) 2343 2344 // Then we make sure that a split transaction can be created for 2345 // any remaining orders without using the utxos returned by 2346 // orderWithLeastOverFund. 2347 if len(newRemainingOrders) > 0 { 2348 canFund, newSplitCoins, newSpents := dcr.fundMultiSplitTx(newRemainingOrders, remainingUTXOs, 2349 splitTxFeeRate, maxFeeRate, splitBuffer, keep, maxLock-totalFunded) 2350 if !canFund { 2351 break 2352 } 2353 splitCoins = newSplitCoins 2354 splitSpents = newSpents 2355 } 2356 2357 indexToFundingCoins[remainingIndexes[orderIndex]] = fundingUTXOs 2358 remainingOrders = newRemainingOrders 2359 remainingIndexes = newRemainingIndexes 2360 } 2361 2362 var splitOutputCoins []asset.Coins 2363 var splitFees uint64 2364 2365 // This should always be true, otherwise this function would not have been 2366 // called. 2367 if len(remainingOrders) > 0 { 2368 splitOutputCoins, splitFees, err = dcr.submitMultiSplitTx(splitCoins, 2369 splitSpents, remainingOrders, maxFeeRate, splitTxFeeRate, splitBuffer) 2370 if err != nil { 2371 return nil, nil, 0, fmt.Errorf("error creating split transaction: %w", err) 2372 } 2373 } 2374 2375 coins := make([]asset.Coins, len(values)) 2376 redeemScripts := make([][]dex.Bytes, len(values)) 2377 spents := make([]*fundingCoin, 0, len(values)) 2378 2379 var splitIndex int 2380 2381 for i := range values { 2382 if fundingUTXOs, ok := indexToFundingCoins[i]; ok { 2383 coins[i] = make(asset.Coins, len(fundingUTXOs)) 2384 redeemScripts[i] = make([]dex.Bytes, len(fundingUTXOs)) 2385 for j, unspent := range fundingUTXOs { 2386 txHash, err := chainhash.NewHashFromStr(unspent.rpc.TxID) 2387 if err != nil { 2388 return nil, nil, 0, fmt.Errorf("error decoding txid from rpc server %s: %w", unspent.rpc.TxID, err) 2389 } 2390 output := newOutput(txHash, unspent.rpc.Vout, toAtoms(unspent.rpc.Amount), unspent.rpc.Tree) 2391 coins[i][j] = output 2392 fc := &fundingCoin{ 2393 op: output, 2394 addr: unspent.rpc.Address, 2395 } 2396 spents = append(spents, fc) 2397 redeemScript, err := hex.DecodeString(unspent.rpc.RedeemScript) 2398 if err != nil { 2399 return nil, nil, 0, fmt.Errorf("error decoding redeem script for %s, script = %s: %w", 2400 txHash, unspent.rpc.RedeemScript, err) 2401 } 2402 redeemScripts[i][j] = redeemScript 2403 } 2404 } else { 2405 coins[i] = splitOutputCoins[splitIndex] 2406 redeemScripts[i] = []dex.Bytes{nil} 2407 splitIndex++ 2408 } 2409 } 2410 2411 dcr.lockFundingCoins(spents) 2412 2413 return coins, redeemScripts, splitFees, nil 2414 } 2415 2416 // fundMulti first attempts to fund each of the orders with with the available 2417 // UTXOs. If a split is not allowed, it will fund the orders that it was able 2418 // to fund. If splitting is allowed, a split transaction will be created to fund 2419 // all of the orders. 2420 func (dcr *ExchangeWallet) fundMulti(maxLock uint64, values []*asset.MultiOrderValue, splitTxFeeRate, maxFeeRate uint64, allowSplit bool, splitBuffer float64) ([]asset.Coins, [][]dex.Bytes, uint64, error) { 2421 dcr.fundingMtx.Lock() 2422 defer dcr.fundingMtx.Unlock() 2423 2424 reserves := dcr.bondReserves.Load() 2425 2426 coins, redeemScripts, fundingCoins, err := dcr.fundMultiBestEffort(reserves, maxLock, values, maxFeeRate, allowSplit) 2427 if err != nil { 2428 return nil, nil, 0, err 2429 } 2430 if len(coins) == len(values) || !allowSplit { 2431 dcr.lockFundingCoins(fundingCoins) 2432 return coins, redeemScripts, 0, nil 2433 } 2434 2435 return dcr.fundMultiWithSplit(reserves, maxLock, values, splitTxFeeRate, maxFeeRate, splitBuffer) 2436 } 2437 2438 func (dcr *ExchangeWallet) FundMultiOrder(mo *asset.MultiOrder, maxLock uint64) (coins []asset.Coins, redeemScripts [][]dex.Bytes, fundingFees uint64, err error) { 2439 var totalRequiredForOrders uint64 2440 for _, value := range mo.Values { 2441 if value.Value == 0 { 2442 return nil, nil, 0, fmt.Errorf("cannot fund value = 0") 2443 } 2444 if value.MaxSwapCount == 0 { 2445 return nil, nil, 0, fmt.Errorf("cannot fund zero-lot order") 2446 } 2447 req := calc.RequiredOrderFunds(value.Value, dexdcr.P2PKHInputSize, value.MaxSwapCount, 2448 dexdcr.InitTxSizeBase, dexdcr.InitTxSize, mo.MaxFeeRate) 2449 totalRequiredForOrders += req 2450 } 2451 2452 if maxLock < totalRequiredForOrders && maxLock != 0 { 2453 return nil, nil, 0, fmt.Errorf("maxLock < totalRequiredForOrders (%d < %d)", maxLock, totalRequiredForOrders) 2454 } 2455 2456 if mo.FeeSuggestion > mo.MaxFeeRate { 2457 return nil, nil, 0, fmt.Errorf("fee suggestion %d > max fee rate %d", mo.FeeSuggestion, mo.MaxFeeRate) 2458 } 2459 2460 cfg := dcr.config() 2461 if cfg.feeRateLimit < mo.MaxFeeRate { 2462 return nil, nil, 0, fmt.Errorf( 2463 "%v: server's max fee rate %v higher than configured fee rate limit %v", 2464 dex.BipIDSymbol(BipID), mo.MaxFeeRate, cfg.feeRateLimit) 2465 } 2466 2467 bal, err := dcr.Balance() 2468 if err != nil { 2469 return nil, nil, 0, fmt.Errorf("error getting balance: %w", err) 2470 } 2471 if bal.Available < totalRequiredForOrders { 2472 return nil, nil, 0, fmt.Errorf("insufficient funds. %d < %d", bal.Available, totalRequiredForOrders) 2473 } 2474 2475 customCfg, err := decodeFundMultiOptions(mo.Options) 2476 if err != nil { 2477 return nil, nil, 0, fmt.Errorf("error decoding options: %w", err) 2478 } 2479 2480 return dcr.fundMulti(maxLock, mo.Values, mo.FeeSuggestion, mo.MaxFeeRate, customCfg.Split, customCfg.SplitBuffer) 2481 } 2482 2483 // fundOrder finds coins from a set of UTXOs for a specified value. This method 2484 // is the same as "fund", except the UTXOs must be passed in, and fundingMtx 2485 // must be held by the caller. 2486 func (dcr *ExchangeWallet) fundInternalWithUTXOs(utxos []*compositeUTXO, keep uint64, // leave utxos for this reserve amt 2487 enough func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64), lock bool) ( 2488 coins asset.Coins, redeemScripts []dex.Bytes, spents []*fundingCoin, sum, size uint64, err error) { 2489 avail := sumUTXOs(utxos) 2490 if keep > avail { // skip utxo selection if we can't possibly make reserves 2491 return nil, nil, nil, 0, 0, asset.ErrInsufficientBalance 2492 } 2493 2494 var sz uint32 2495 2496 // First take some UTXOs out of the mix for any keep amount. Select these 2497 // with the objective of being as close to the amount as possible, unlike 2498 // tryFund that minimizes the number of UTXOs chosen. By doing this first, 2499 // we may be making the order spend a larger number of UTXOs, but we 2500 // mitigate subsequent order funding failure due to reserves because we know 2501 // this order will leave behind sufficient UTXOs without relying on change. 2502 if keep > 0 { 2503 kept := leastOverFund(reserveEnough(keep), utxos) 2504 dcr.log.Debugf("Setting aside %v DCR in %d UTXOs to respect the %v DCR reserved amount", 2505 toDCR(sumUTXOs(kept)), len(kept), toDCR(keep)) 2506 utxosPruned := utxoSetDiff(utxos, kept) 2507 sum, _, sz, coins, spents, redeemScripts, err = tryFund(utxosPruned, enough) 2508 if err != nil { // try with the full set 2509 dcr.log.Debugf("Unable to fund order with UTXOs set aside (%v), trying again with full UTXO set.", err) 2510 } // else spents is populated 2511 } 2512 if len(spents) == 0 { // either keep is zero or it failed with utxosPruned 2513 // Without utxos set aside for keep, we have to consider any spendable 2514 // change (extra) that the enough func grants us. 2515 var extra uint64 2516 sum, extra, sz, coins, spents, redeemScripts, err = tryFund(utxos, enough) 2517 if err != nil { 2518 return nil, nil, nil, 0, 0, err 2519 } 2520 if avail-sum+extra < keep { 2521 return nil, nil, nil, 0, 0, asset.ErrInsufficientBalance 2522 } 2523 // else we got lucky with the legacy funding approach and there was 2524 // either available unspent or the enough func granted spendable change. 2525 if keep > 0 && extra > 0 { 2526 dcr.log.Debugf("Funding succeeded with %f DCR in spendable change.", toDCR(extra)) 2527 } 2528 } 2529 2530 if lock { 2531 err = dcr.lockFundingCoins(spents) 2532 if err != nil { 2533 return nil, nil, nil, 0, 0, err 2534 } 2535 } 2536 return coins, redeemScripts, spents, sum, uint64(sz), nil 2537 } 2538 2539 // fund finds coins for the specified value. A function is provided that can 2540 // check whether adding the provided output would be enough to satisfy the 2541 // needed value. Preference is given to selecting coins with 1 or more confs, 2542 // falling back to 0-conf coins where there are not enough 1+ confs coins. If 2543 // change should not be considered "kept" (e.g. no preceding split txn, or 2544 // mixing sends change to umixed account where it is unusable for reserves), 2545 // caller should return 0 extra from enough func. 2546 func (dcr *ExchangeWallet) fund(keep uint64, // leave utxos for this reserve amt 2547 enough func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64)) ( 2548 coins asset.Coins, redeemScripts []dex.Bytes, sum, size uint64, err error) { 2549 2550 // Keep a consistent view of spendable and locked coins in the wallet and 2551 // the fundingCoins map to make this safe for concurrent use. 2552 dcr.fundingMtx.Lock() // before listing unspents in wallet 2553 defer dcr.fundingMtx.Unlock() // hold until lockFundingCoins (wallet and map) 2554 2555 utxos, err := dcr.spendableUTXOs() 2556 if err != nil { 2557 return nil, nil, 0, 0, err 2558 } 2559 2560 coins, redeemScripts, _, sum, size, err = dcr.fundInternalWithUTXOs(utxos, keep, enough, true) 2561 return coins, redeemScripts, sum, size, err 2562 } 2563 2564 // spendableUTXOs generates a slice of spendable *compositeUTXO. 2565 func (dcr *ExchangeWallet) spendableUTXOs() ([]*compositeUTXO, error) { 2566 accts := dcr.wallet.Accounts() 2567 unspents, err := dcr.wallet.Unspents(dcr.ctx, accts.PrimaryAccount) 2568 if err != nil { 2569 return nil, err 2570 } 2571 if accts.TradingAccount != "" { 2572 // Trading account may contain spendable utxos such as unspent split tx 2573 // outputs that are unlocked/returned. TODO: Care should probably be 2574 // taken to ensure only unspent split tx outputs are selected and other 2575 // unmixed outputs in the trading account are ignored. 2576 tradingAcctSpendables, err := dcr.wallet.Unspents(dcr.ctx, accts.TradingAccount) 2577 if err != nil { 2578 return nil, err 2579 } 2580 unspents = append(unspents, tradingAcctSpendables...) 2581 } 2582 if len(unspents) == 0 { 2583 return nil, fmt.Errorf("insufficient funds. 0 DCR available to spend in account %q", accts.PrimaryAccount) 2584 } 2585 2586 // Parse utxos to include script size for spending input. Returned utxos 2587 // will be sorted in ascending order by amount (smallest first). 2588 utxos, err := dcr.parseUTXOs(unspents) 2589 if err != nil { 2590 return nil, fmt.Errorf("error parsing unspent outputs: %w", err) 2591 } 2592 if len(utxos) == 0 { 2593 return nil, fmt.Errorf("no funds available") 2594 } 2595 return utxos, nil 2596 } 2597 2598 // tryFund attempts to use the provided UTXO set to satisfy the enough function 2599 // with the fewest number of inputs. The selected utxos are not locked. If the 2600 // requirement can be satisfied without 0-conf utxos, that set will be selected 2601 // regardless of whether the 0-conf inclusive case would be cheaper. The 2602 // provided UTXOs must be sorted in ascending order by value. 2603 func tryFund(utxos []*compositeUTXO, 2604 enough func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64)) ( 2605 sum, extra uint64, size uint32, coins asset.Coins, spents []*fundingCoin, redeemScripts []dex.Bytes, err error) { 2606 2607 addUTXO := func(unspent *compositeUTXO) error { 2608 txHash, err := chainhash.NewHashFromStr(unspent.rpc.TxID) 2609 if err != nil { 2610 return fmt.Errorf("error decoding txid: %w", err) 2611 } 2612 v := toAtoms(unspent.rpc.Amount) 2613 redeemScript, err := hex.DecodeString(unspent.rpc.RedeemScript) 2614 if err != nil { 2615 return fmt.Errorf("error decoding redeem script for %s, script = %s: %w", 2616 unspent.rpc.TxID, unspent.rpc.RedeemScript, err) 2617 } 2618 op := newOutput(txHash, unspent.rpc.Vout, v, unspent.rpc.Tree) 2619 coins = append(coins, op) 2620 spents = append(spents, &fundingCoin{ 2621 op: op, 2622 addr: unspent.rpc.Address, 2623 }) 2624 redeemScripts = append(redeemScripts, redeemScript) 2625 size += unspent.input.Size() 2626 sum += v 2627 return nil 2628 } 2629 2630 isEnoughWith := func(utxo *compositeUTXO) bool { 2631 ok, _ := enough(sum, size, utxo) 2632 return ok 2633 } 2634 2635 tryUTXOs := func(minconf int64) (ok bool, err error) { 2636 sum, size = 0, 0 // size is only sum of inputs size, not including tx overhead or outputs 2637 coins, spents, redeemScripts = nil, nil, nil 2638 2639 okUTXOs := make([]*compositeUTXO, 0, len(utxos)) // over-allocate 2640 for _, cu := range utxos { 2641 if cu.confs >= minconf && cu.rpc.Spendable { 2642 okUTXOs = append(okUTXOs, cu) 2643 } 2644 } 2645 2646 for { 2647 // If there are none left, we don't have enough. 2648 if len(okUTXOs) == 0 { 2649 return false, nil 2650 } 2651 2652 // Check if the largest output is too small. 2653 lastUTXO := okUTXOs[len(okUTXOs)-1] 2654 if !isEnoughWith(lastUTXO) { 2655 if err = addUTXO(lastUTXO); err != nil { 2656 return false, err 2657 } 2658 okUTXOs = okUTXOs[0 : len(okUTXOs)-1] 2659 continue 2660 } 2661 2662 // We only need one then. Find it. 2663 idx := sort.Search(len(okUTXOs), func(i int) bool { 2664 return isEnoughWith(okUTXOs[i]) 2665 }) 2666 // No need to check idx == n. We already verified that the last 2667 // utxo passes above. 2668 final := okUTXOs[idx] 2669 _, extra = enough(sum, size, final) // sort.Search might not have called isEnough for this utxo last 2670 if err = addUTXO(final); err != nil { 2671 return false, err 2672 } 2673 return true, nil 2674 } 2675 } 2676 2677 // First try with confs>0. 2678 ok, err := tryUTXOs(1) 2679 if err != nil { 2680 return 0, 0, 0, nil, nil, nil, err 2681 } 2682 2683 // Fallback to allowing 0-conf outputs. 2684 if !ok { 2685 ok, err = tryUTXOs(0) 2686 if err != nil { 2687 return 0, 0, 0, nil, nil, nil, err 2688 } 2689 if !ok { 2690 return 0, 0, 0, nil, nil, nil, fmt.Errorf("not enough to cover requested funds. "+ 2691 "%s DCR available in %d UTXOs (%w)", amount(sum), len(coins), asset.ErrInsufficientBalance) 2692 } 2693 } 2694 2695 return 2696 } 2697 2698 // split will send a split transaction and return the sized output. If the 2699 // split transaction is determined to be un-economical, it will not be sent, 2700 // there is no error, and the input coins will be returned unmodified, but an 2701 // info message will be logged. The returned bool indicates if a split tx was 2702 // sent (true) or if the original coins were returned unmodified (false). 2703 // 2704 // A split transaction nets additional network bytes consisting of 2705 // - overhead from 1 transaction 2706 // - 1 extra signed p2pkh-spending input. The split tx has the fundingCoins as 2707 // inputs now, but we'll add the input that spends the sized coin that will go 2708 // into the first swap 2709 // - 2 additional p2pkh outputs for the split tx sized output and change 2710 // 2711 // If the fees associated with this extra baggage are more than the excess 2712 // amount that would be locked if a split transaction were not used, then the 2713 // split transaction is pointless. This might be common, for instance, if an 2714 // order is canceled partially filled, and then the remainder resubmitted. We 2715 // would already have an output of just the right size, and that would be 2716 // recognized here. 2717 func (dcr *ExchangeWallet) split(value uint64, lots uint64, coins asset.Coins, inputsSize uint64, 2718 splitFeeRate, bumpedMaxRate, extraOutput uint64) (asset.Coins, bool, uint64, error) { 2719 2720 // Calculate the extra fees associated with the additional inputs, outputs, 2721 // and transaction overhead, and compare to the excess that would be locked. 2722 baggageFees := bumpedMaxRate * splitTxBaggage 2723 if extraOutput > 0 { 2724 baggageFees += bumpedMaxRate * dexdcr.P2PKHOutputSize 2725 } 2726 2727 var coinSum uint64 2728 for _, coin := range coins { 2729 coinSum += coin.Value() 2730 } 2731 2732 valStr := amount(value).String() 2733 2734 excess := coinSum - calc.RequiredOrderFunds(value, inputsSize, lots, 2735 dexdcr.InitTxSizeBase, dexdcr.InitTxSize, bumpedMaxRate) 2736 2737 if baggageFees > excess { 2738 dcr.log.Debugf("Skipping split transaction because cost is greater than potential over-lock. %s > %s.", 2739 amount(baggageFees), amount(excess)) 2740 dcr.log.Infof("Funding %s DCR order with coins %v worth %s", valStr, coins, amount(coinSum)) 2741 // NOTE: The caller may be expecting a split to happen to maintain 2742 // reserves via the change from the split, but the amount held locked 2743 // when skipping the split in this case is roughly equivalent to the 2744 // loss to fees in a split. This trivial amount is of no concern because 2745 // the reserves should be buffered for amounts much larger than the fees 2746 // on a single transaction. 2747 return coins, false, 0, nil 2748 } 2749 2750 // Generate an address to receive the sized outputs. If mixing is enabled on 2751 // the wallet, generate the address from the external branch of the trading 2752 // account. The external branch is used so that if this split output isn't 2753 // spent, it won't be transferred to the unmixed account for re-mixing. 2754 // Instead, it'll simply be unlocked in the trading account and can thus be 2755 // used to fund future orders. 2756 accts := dcr.wallet.Accounts() 2757 getAddr := func() (stdaddr.Address, error) { 2758 if accts.TradingAccount != "" { 2759 return dcr.wallet.ExternalAddress(dcr.ctx, accts.TradingAccount) 2760 } 2761 return dcr.wallet.ExternalAddress(dcr.ctx, accts.PrimaryAccount) 2762 } 2763 addr, err := getAddr() 2764 if err != nil { 2765 return nil, false, 0, fmt.Errorf("error creating split transaction address: %w", err) 2766 } 2767 2768 var addr2 stdaddr.Address 2769 if extraOutput > 0 { 2770 addr2, err = getAddr() 2771 if err != nil { 2772 return nil, false, 0, fmt.Errorf("error creating secondary split transaction address: %w", err) 2773 } 2774 } 2775 2776 reqFunds := calc.RequiredOrderFunds(value, dexdcr.P2PKHInputSize, lots, 2777 dexdcr.InitTxSizeBase, dexdcr.InitTxSize, bumpedMaxRate) 2778 2779 dcr.fundingMtx.Lock() // before generating the new output in sendCoins 2780 defer dcr.fundingMtx.Unlock() // after locking it (wallet and map) 2781 2782 msgTx, sentVal, err := dcr.sendCoins(coins, addr, addr2, reqFunds, extraOutput, splitFeeRate, false) 2783 if err != nil { 2784 return nil, false, 0, fmt.Errorf("error sending split transaction: %w", err) 2785 } 2786 2787 if sentVal != reqFunds { 2788 dcr.log.Errorf("split - total sent %.8f does not match expected %.8f", toDCR(sentVal), toDCR(reqFunds)) 2789 } 2790 2791 op := newOutput(msgTx.CachedTxHash(), 0, sentVal, wire.TxTreeRegular) 2792 2793 // Lock the funding coin. 2794 err = dcr.lockFundingCoins([]*fundingCoin{{ 2795 op: op, 2796 addr: addr.String(), 2797 }}) 2798 if err != nil { 2799 dcr.log.Errorf("error locking funding coin from split transaction %s", op) 2800 } 2801 2802 // Unlock the spent coins. 2803 _, err = dcr.returnCoins(coins) 2804 if err != nil { 2805 dcr.log.Errorf("error returning coins spent in split transaction %v", coins) 2806 } 2807 2808 totalOut := uint64(0) 2809 for i := 0; i < len(msgTx.TxOut); i++ { 2810 totalOut += uint64(msgTx.TxOut[i].Value) 2811 } 2812 2813 txHash := msgTx.CachedTxHash() 2814 dcr.addTxToHistory(&asset.WalletTransaction{ 2815 Type: asset.Split, 2816 ID: txHash.String(), 2817 Fees: coinSum - totalOut, 2818 }, txHash, true) 2819 2820 dcr.log.Infof("Funding %s DCR order with split output coin %v from original coins %v", valStr, op, coins) 2821 dcr.log.Infof("Sent split transaction %s to accommodate swap of size %s + fees = %s DCR", 2822 op.txHash(), valStr, amount(reqFunds)) 2823 2824 return asset.Coins{op}, true, coinSum - totalOut, nil 2825 } 2826 2827 // lockFundingCoins locks the funding coins via RPC and stores them in the map. 2828 // This function is not safe for concurrent use. The caller should lock 2829 // dcr.fundingMtx. 2830 func (dcr *ExchangeWallet) lockFundingCoins(fCoins []*fundingCoin) error { 2831 wireOPs := make([]*wire.OutPoint, 0, len(fCoins)) 2832 for _, c := range fCoins { 2833 wireOPs = append(wireOPs, wire.NewOutPoint(c.op.txHash(), c.op.vout(), c.op.tree)) 2834 } 2835 err := dcr.wallet.LockUnspent(dcr.ctx, false, wireOPs) 2836 if err != nil { 2837 return err 2838 } 2839 for _, c := range fCoins { 2840 dcr.fundingCoins[c.op.pt] = c 2841 } 2842 return nil 2843 } 2844 2845 func (dcr *ExchangeWallet) unlockFundingCoins(fCoins []*fundingCoin) error { 2846 wireOPs := make([]*wire.OutPoint, 0, len(fCoins)) 2847 for _, c := range fCoins { 2848 wireOPs = append(wireOPs, wire.NewOutPoint(c.op.txHash(), c.op.vout(), c.op.tree)) 2849 } 2850 err := dcr.wallet.LockUnspent(dcr.ctx, true, wireOPs) 2851 if err != nil { 2852 return err 2853 } 2854 for _, c := range fCoins { 2855 delete(dcr.fundingCoins, c.op.pt) 2856 } 2857 return nil 2858 } 2859 2860 // ReturnCoins unlocks coins. This would be necessary in the case of a canceled 2861 // order. Coins belonging to the tradingAcct, if configured, are transferred to 2862 // the unmixed account with the exception of unspent split tx outputs which are 2863 // kept in the tradingAcct and may later be used to fund future orders. If 2864 // called with a nil slice, all coins are returned and none are moved to the 2865 // unmixed account. 2866 func (dcr *ExchangeWallet) ReturnCoins(unspents asset.Coins) error { 2867 if unspents == nil { // not just empty to make this harder to do accidentally 2868 dcr.log.Debugf("Returning all coins.") 2869 dcr.fundingMtx.Lock() 2870 defer dcr.fundingMtx.Unlock() 2871 if err := dcr.wallet.LockUnspent(dcr.ctx, true, nil); err != nil { 2872 return err 2873 } 2874 dcr.fundingCoins = make(map[outPoint]*fundingCoin) 2875 return nil 2876 } 2877 if len(unspents) == 0 { 2878 return fmt.Errorf("cannot return zero coins") 2879 } 2880 2881 dcr.fundingMtx.Lock() 2882 returnedCoins, err := dcr.returnCoins(unspents) 2883 dcr.fundingMtx.Unlock() 2884 accts := dcr.wallet.Accounts() 2885 if err != nil || accts.UnmixedAccount == "" { 2886 return err 2887 } 2888 2889 // If any of these coins belong to the trading account, transfer them to the 2890 // unmixed account to be re-mixed into the primary account before being 2891 // re-selected for funding future orders. This doesn't apply to unspent 2892 // split tx outputs, which should remain in the trading account and be 2893 // selected from there for funding future orders. 2894 var coinsToTransfer []asset.Coin 2895 for _, coin := range returnedCoins { 2896 if coin.addr == "" { 2897 txOut, err := dcr.wallet.UnspentOutput(dcr.ctx, coin.op.txHash(), coin.op.vout(), coin.op.tree) 2898 if err != nil { 2899 dcr.log.Errorf("wallet.UnspentOutput error for returned coin %s: %v", coin.op, err) 2900 continue 2901 } 2902 if len(txOut.Addresses) == 0 { 2903 dcr.log.Errorf("no address in gettxout response for returned coin %s", coin.op) 2904 continue 2905 } 2906 coin.addr = txOut.Addresses[0] 2907 } 2908 addrInfo, err := dcr.wallet.AddressInfo(dcr.ctx, coin.addr) 2909 if err != nil { 2910 dcr.log.Errorf("wallet.AddressInfo error for returned coin %s: %v", coin.op, err) 2911 continue 2912 } 2913 // Move this coin to the unmixed account if it was sent to the internal 2914 // branch of the trading account. This excludes unspent split tx outputs 2915 // which are sent to the external branch of the trading account. 2916 if addrInfo.Branch == acctInternalBranch && addrInfo.Account == accts.TradingAccount { 2917 coinsToTransfer = append(coinsToTransfer, coin.op) 2918 } 2919 } 2920 2921 if len(coinsToTransfer) > 0 { 2922 tx, totalSent, err := dcr.sendAll(coinsToTransfer, accts.UnmixedAccount) 2923 if err != nil { 2924 dcr.log.Errorf("unable to transfer unlocked swapped change from temp trading "+ 2925 "account to unmixed account: %v", err) 2926 } else { 2927 dcr.log.Infof("Transferred %s from temp trading account to unmixed account in tx %s.", 2928 dcrutil.Amount(totalSent), tx.TxHash()) 2929 } 2930 } 2931 2932 return nil 2933 } 2934 2935 // returnCoins unlocks coins and removes them from the fundingCoins map. 2936 // Requires fundingMtx to be write-locked. 2937 func (dcr *ExchangeWallet) returnCoins(unspents asset.Coins) ([]*fundingCoin, error) { 2938 if len(unspents) == 0 { 2939 return nil, fmt.Errorf("cannot return zero coins") 2940 } 2941 2942 ops := make([]*wire.OutPoint, 0, len(unspents)) 2943 fundingCoins := make([]*fundingCoin, 0, len(unspents)) 2944 2945 dcr.log.Debugf("returning coins %s", unspents) 2946 for _, unspent := range unspents { 2947 op, err := dcr.convertCoin(unspent) 2948 if err != nil { 2949 return nil, fmt.Errorf("error converting coin: %w", err) 2950 } 2951 ops = append(ops, op.wireOutPoint()) // op.tree may be wire.TxTreeUnknown, but that's fine since wallet.LockUnspent doesn't rely on it 2952 if fCoin, ok := dcr.fundingCoins[op.pt]; ok { 2953 fundingCoins = append(fundingCoins, fCoin) 2954 } else { 2955 dcr.log.Warnf("returning coin %s that is not cached as a funding coin", op) 2956 fundingCoins = append(fundingCoins, &fundingCoin{op: op}) 2957 } 2958 } 2959 2960 if err := dcr.wallet.LockUnspent(dcr.ctx, true, ops); err != nil { 2961 return nil, err 2962 } 2963 2964 for _, fCoin := range fundingCoins { 2965 delete(dcr.fundingCoins, fCoin.op.pt) 2966 } 2967 2968 return fundingCoins, nil 2969 } 2970 2971 // FundingCoins gets funding coins for the coin IDs. The coins are locked. This 2972 // method might be called to reinitialize an order from data stored externally. 2973 // This method will only return funding coins, e.g. unspent transaction outputs. 2974 func (dcr *ExchangeWallet) FundingCoins(ids []dex.Bytes) (asset.Coins, error) { 2975 // First check if we have the coins in cache. 2976 coins := make(asset.Coins, 0, len(ids)) 2977 notFound := make(map[outPoint]bool) 2978 dcr.fundingMtx.Lock() 2979 defer dcr.fundingMtx.Unlock() // stay locked until we update the map and lock them in the wallet 2980 for _, id := range ids { 2981 txHash, vout, err := decodeCoinID(id) 2982 if err != nil { 2983 return nil, err 2984 } 2985 pt := newOutPoint(txHash, vout) 2986 fundingCoin, found := dcr.fundingCoins[pt] 2987 if found { 2988 coins = append(coins, fundingCoin.op) 2989 continue 2990 } 2991 notFound[pt] = true 2992 } 2993 if len(notFound) == 0 { 2994 return coins, nil 2995 } 2996 2997 // Check locked outputs for not found coins. 2998 for _, acct := range dcr.fundingAccounts() { 2999 lockedOutputs, err := dcr.wallet.LockedOutputs(dcr.ctx, acct) 3000 if err != nil { 3001 return nil, err 3002 } 3003 for _, output := range lockedOutputs { 3004 txHash, err := chainhash.NewHashFromStr(output.Txid) 3005 if err != nil { 3006 return nil, fmt.Errorf("error decoding txid from rpc server %s: %w", output.Txid, err) 3007 } 3008 pt := newOutPoint(txHash, output.Vout) 3009 if !notFound[pt] { 3010 continue 3011 } 3012 txOut, err := dcr.wallet.UnspentOutput(dcr.ctx, txHash, output.Vout, output.Tree) 3013 if err != nil { 3014 return nil, fmt.Errorf("gettxout error for locked output %v: %w", pt.String(), err) 3015 } 3016 var address string 3017 if len(txOut.Addresses) > 0 { 3018 address = txOut.Addresses[0] 3019 } 3020 coin := newOutput(txHash, output.Vout, toAtoms(output.Amount), output.Tree) 3021 coins = append(coins, coin) 3022 dcr.fundingCoins[pt] = &fundingCoin{ 3023 op: coin, 3024 addr: address, 3025 } 3026 delete(notFound, pt) 3027 if len(notFound) == 0 { 3028 return coins, nil 3029 } 3030 } 3031 } 3032 3033 // Some funding coins still not found after checking locked outputs. 3034 // Check wallet unspent outputs as last resort. Lock the coins if found. 3035 coinsToLock := make([]*wire.OutPoint, 0, len(notFound)) 3036 for _, acct := range dcr.fundingAccounts() { 3037 unspents, err := dcr.wallet.Unspents(dcr.ctx, acct) 3038 if err != nil { 3039 return nil, err 3040 } 3041 for _, txout := range unspents { 3042 txHash, err := chainhash.NewHashFromStr(txout.TxID) 3043 if err != nil { 3044 return nil, fmt.Errorf("error decoding txid from rpc server %s: %w", txout.TxID, err) 3045 } 3046 pt := newOutPoint(txHash, txout.Vout) 3047 if !notFound[pt] { 3048 continue 3049 } 3050 coinsToLock = append(coinsToLock, wire.NewOutPoint(txHash, txout.Vout, txout.Tree)) 3051 coin := newOutput(txHash, txout.Vout, toAtoms(txout.Amount), txout.Tree) 3052 coins = append(coins, coin) 3053 dcr.fundingCoins[pt] = &fundingCoin{ 3054 op: coin, 3055 addr: txout.Address, 3056 } 3057 delete(notFound, pt) 3058 if len(notFound) == 0 { 3059 break 3060 } 3061 } 3062 } 3063 3064 // Return an error if some coins are still not found. 3065 if len(notFound) != 0 { 3066 ids := make([]string, 0, len(notFound)) 3067 for pt := range notFound { 3068 ids = append(ids, pt.String()) 3069 } 3070 return nil, fmt.Errorf("funding coins not found: %s", strings.Join(ids, ", ")) 3071 } 3072 3073 dcr.log.Debugf("Locking funding coins that were unlocked %v", coinsToLock) 3074 err := dcr.wallet.LockUnspent(dcr.ctx, false, coinsToLock) 3075 if err != nil { 3076 return nil, err 3077 } 3078 3079 return coins, nil 3080 } 3081 3082 // Swap sends the swaps in a single transaction. The Receipts returned can be 3083 // used to refund a failed transaction. The Input coins are manually unlocked 3084 // because they're not auto-unlocked by the wallet and therefore inaccurately 3085 // included as part of the locked balance despite being spent. 3086 func (dcr *ExchangeWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint64, error) { 3087 if swaps.FeeRate == 0 { 3088 return nil, nil, 0, fmt.Errorf("cannot send swap with with zero fee rate") 3089 } 3090 3091 var totalOut uint64 3092 // Start with an empty MsgTx. 3093 baseTx := wire.NewMsgTx() 3094 // Add the funding utxos. 3095 totalIn, err := dcr.addInputCoins(baseTx, swaps.Inputs) 3096 if err != nil { 3097 return nil, nil, 0, err 3098 } 3099 3100 customCfg := new(swapOptions) 3101 err = config.Unmapify(swaps.Options, customCfg) 3102 if err != nil { 3103 return nil, nil, 0, fmt.Errorf("error parsing swap options: %w", err) 3104 } 3105 3106 contracts := make([][]byte, 0, len(swaps.Contracts)) 3107 refundAddrs := make([]stdaddr.Address, 0, len(swaps.Contracts)) 3108 // Add the contract outputs. 3109 for _, contract := range swaps.Contracts { 3110 totalOut += contract.Value 3111 // revokeAddrV2 is the address belonging to the key that will be 3112 // used to sign and refund a swap past its encoded refund locktime. 3113 revokeAddrV2, err := dcr.wallet.ExternalAddress(dcr.ctx, dcr.depositAccount()) 3114 if err != nil { 3115 return nil, nil, 0, fmt.Errorf("error creating revocation address: %w", err) 3116 } 3117 refundAddrs = append(refundAddrs, revokeAddrV2) 3118 // Create the contract, a P2SH redeem script. 3119 contractScript, err := dexdcr.MakeContract(contract.Address, revokeAddrV2.String(), contract.SecretHash, int64(contract.LockTime), dcr.chainParams) 3120 if err != nil { 3121 return nil, nil, 0, fmt.Errorf("unable to create pubkey script for address %s: %w", contract.Address, err) 3122 } 3123 contracts = append(contracts, contractScript) 3124 // Make the P2SH address and pubkey script. 3125 scriptAddr, err := stdaddr.NewAddressScriptHashV0(contractScript, dcr.chainParams) 3126 if err != nil { 3127 return nil, nil, 0, fmt.Errorf("error encoding script address: %w", err) 3128 } 3129 p2shScriptVer, p2shScript := scriptAddr.PaymentScript() 3130 // Add the transaction output. 3131 txOut := newTxOut(int64(contract.Value), p2shScriptVer, p2shScript) 3132 baseTx.AddTxOut(txOut) 3133 } 3134 if totalIn < totalOut { 3135 return nil, nil, 0, fmt.Errorf("unfunded contract. %d < %d", totalIn, totalOut) 3136 } 3137 3138 // Ensure we have enough outputs before broadcasting. 3139 swapCount := len(swaps.Contracts) 3140 if len(baseTx.TxOut) < swapCount { 3141 return nil, nil, 0, fmt.Errorf("fewer outputs than swaps. %d < %d", len(baseTx.TxOut), swapCount) 3142 } 3143 3144 feeRate, err := calcBumpedRate(swaps.FeeRate, customCfg.FeeBump) 3145 if err != nil { 3146 dcr.log.Errorf("ignoring invalid fee bump factor, %s: %v", float64PtrStr(customCfg.FeeBump), err) 3147 } 3148 3149 // Add change, sign, and send the transaction. 3150 dcr.fundingMtx.Lock() // before generating change output 3151 defer dcr.fundingMtx.Unlock() // hold until after returnCoins and lockFundingCoins(change) 3152 // Sign the tx but don't send the transaction yet until 3153 // the individual swap refund txs are prepared and signed. 3154 changeAcct := dcr.depositAccount() 3155 tradingAccount := dcr.wallet.Accounts().TradingAccount 3156 if swaps.LockChange && tradingAccount != "" { 3157 // Change will likely be used to fund more swaps, send to trading 3158 // account. 3159 changeAcct = tradingAccount 3160 } 3161 msgTx, change, changeAddr, fees, err := dcr.signTxAndAddChange(baseTx, feeRate, -1, changeAcct) 3162 if err != nil { 3163 return nil, nil, 0, err 3164 } 3165 3166 receipts := make([]asset.Receipt, 0, swapCount) 3167 txHash := msgTx.CachedTxHash() 3168 for i, contract := range swaps.Contracts { 3169 output := newOutput(txHash, uint32(i), contract.Value, wire.TxTreeRegular) 3170 signedRefundTx, _, _, err := dcr.refundTx(output.ID(), contracts[i], contract.Value, refundAddrs[i], swaps.FeeRate) 3171 if err != nil { 3172 return nil, nil, 0, fmt.Errorf("error creating refund tx: %w", err) 3173 } 3174 refundB, err := signedRefundTx.Bytes() 3175 if err != nil { 3176 return nil, nil, 0, fmt.Errorf("error serializing refund tx: %w", err) 3177 } 3178 receipts = append(receipts, &swapReceipt{ 3179 output: output, 3180 contract: contracts[i], 3181 expiration: time.Unix(int64(contract.LockTime), 0).UTC(), 3182 signedRefund: refundB, 3183 }) 3184 } 3185 3186 // Refund txs prepared and signed. Can now broadcast the swap(s). 3187 _, err = dcr.broadcastTx(msgTx) 3188 if err != nil { 3189 return nil, nil, 0, err 3190 } 3191 3192 dcr.addTxToHistory(&asset.WalletTransaction{ 3193 Type: asset.Swap, 3194 ID: txHash.String(), 3195 Amount: totalOut, 3196 Fees: fees, 3197 }, txHash, true) 3198 3199 // Return spent outputs. 3200 _, err = dcr.returnCoins(swaps.Inputs) 3201 if err != nil { 3202 dcr.log.Errorf("error unlocking swapped coins", swaps.Inputs) 3203 } 3204 3205 // Lock the change coin, if requested. 3206 if swaps.LockChange { 3207 dcr.log.Debugf("locking change coin %s", change) 3208 err = dcr.lockFundingCoins([]*fundingCoin{{ 3209 op: change, 3210 addr: changeAddr, 3211 }}) 3212 if err != nil { 3213 dcr.log.Warnf("Failed to lock dcr change coin %s", change) 3214 } 3215 } 3216 3217 // If change is nil, return a nil asset.Coin. 3218 var changeCoin asset.Coin 3219 if change != nil { 3220 changeCoin = change 3221 } 3222 return receipts, changeCoin, fees, nil 3223 } 3224 3225 // Redeem sends the redemption transaction, which may contain more than one 3226 // redemption. FeeSuggestion is just a fallback if an internal estimate using 3227 // the wallet's redeem confirm block target setting is not available. 3228 func (dcr *ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, uint64, error) { 3229 // Create a transaction that spends the referenced contract. 3230 msgTx := wire.NewMsgTx() 3231 var totalIn uint64 3232 var contracts [][]byte 3233 var addresses []stdaddr.Address 3234 for _, r := range form.Redemptions { 3235 if r.Spends == nil { 3236 return nil, nil, 0, fmt.Errorf("no audit info") 3237 } 3238 3239 cinfo, err := convertAuditInfo(r.Spends, dcr.chainParams) 3240 if err != nil { 3241 return nil, nil, 0, err 3242 } 3243 3244 // Extract the swap contract recipient and secret hash and check the secret 3245 // hash against the hash of the provided secret. 3246 contract := cinfo.contract 3247 _, receiver, _, secretHash, err := dexdcr.ExtractSwapDetails(contract, dcr.chainParams) 3248 if err != nil { 3249 return nil, nil, 0, fmt.Errorf("error extracting swap addresses: %w", err) 3250 } 3251 checkSecretHash := sha256.Sum256(r.Secret) 3252 if !bytes.Equal(checkSecretHash[:], secretHash) { 3253 return nil, nil, 0, fmt.Errorf("secret hash mismatch. %x != %x", checkSecretHash[:], secretHash) 3254 } 3255 addresses = append(addresses, receiver) 3256 contracts = append(contracts, contract) 3257 prevOut := cinfo.output.wireOutPoint() 3258 txIn := wire.NewTxIn(prevOut, int64(cinfo.output.value), []byte{}) 3259 msgTx.AddTxIn(txIn) 3260 totalIn += cinfo.output.value 3261 } 3262 3263 // Calculate the size and the fees. 3264 size := msgTx.SerializeSize() + dexdcr.RedeemSwapSigScriptSize*len(form.Redemptions) + dexdcr.P2PKHOutputSize 3265 3266 customCfg := new(redeemOptions) 3267 err := config.Unmapify(form.Options, customCfg) 3268 if err != nil { 3269 return nil, nil, 0, fmt.Errorf("error parsing selected swap options: %w", err) 3270 } 3271 3272 rawFeeRate := dcr.targetFeeRateWithFallback(dcr.config().redeemConfTarget, form.FeeSuggestion) 3273 feeRate, err := calcBumpedRate(rawFeeRate, customCfg.FeeBump) 3274 if err != nil { 3275 dcr.log.Errorf("calcBumpRate error: %v", err) 3276 } 3277 fee := feeRate * uint64(size) 3278 if fee > totalIn { 3279 // Double check that the fee bump isn't the issue. 3280 feeRate = rawFeeRate 3281 fee = feeRate * uint64(size) 3282 if fee > totalIn { 3283 return nil, nil, 0, fmt.Errorf("redeem tx not worth the fees") 3284 } 3285 dcr.log.Warnf("Ignoring fee bump (%v) resulting in fees > redemption", float64PtrStr(customCfg.FeeBump)) 3286 } 3287 3288 // Send the funds back to the exchange wallet. 3289 txOut, _, err := dcr.makeExternalOut(dcr.depositAccount(), totalIn-fee) 3290 if err != nil { 3291 return nil, nil, 0, err 3292 } 3293 // One last check for dust. 3294 if dexdcr.IsDust(txOut, feeRate) { 3295 return nil, nil, 0, fmt.Errorf("redeem output is dust") 3296 } 3297 msgTx.AddTxOut(txOut) 3298 // Sign the inputs. 3299 for i, r := range form.Redemptions { 3300 contract := contracts[i] 3301 redeemSig, redeemPubKey, err := dcr.createSig(msgTx, i, contract, addresses[i]) 3302 if err != nil { 3303 return nil, nil, 0, err 3304 } 3305 redeemSigScript, err := dexdcr.RedeemP2SHContract(contract, redeemSig, redeemPubKey, r.Secret) 3306 if err != nil { 3307 return nil, nil, 0, err 3308 } 3309 msgTx.TxIn[i].SignatureScript = redeemSigScript 3310 } 3311 // Send the transaction. 3312 txHash, err := dcr.broadcastTx(msgTx) 3313 if err != nil { 3314 return nil, nil, 0, err 3315 } 3316 3317 dcr.addTxToHistory(&asset.WalletTransaction{ 3318 Type: asset.Redeem, 3319 ID: txHash.String(), 3320 Amount: totalIn, 3321 Fees: fee, 3322 }, txHash, true) 3323 3324 coinIDs := make([]dex.Bytes, 0, len(form.Redemptions)) 3325 dcr.mempoolRedeemsMtx.Lock() 3326 for i := range form.Redemptions { 3327 coinIDs = append(coinIDs, toCoinID(txHash, uint32(i))) 3328 var secretHash [32]byte 3329 copy(secretHash[:], form.Redemptions[i].Spends.SecretHash) 3330 dcr.mempoolRedeems[secretHash] = &mempoolRedeem{txHash: *txHash, firstSeen: time.Now()} 3331 } 3332 dcr.mempoolRedeemsMtx.Unlock() 3333 return coinIDs, newOutput(txHash, 0, uint64(txOut.Value), wire.TxTreeRegular), fee, nil 3334 } 3335 3336 // SignMessage signs the message with the private key associated with the 3337 // specified funding Coin. A slice of pubkeys required to spend the Coin and a 3338 // signature for each pubkey are returned. 3339 func (dcr *ExchangeWallet) SignMessage(coin asset.Coin, msg dex.Bytes) (pubkeys, sigs []dex.Bytes, err error) { 3340 op, err := dcr.convertCoin(coin) 3341 if err != nil { 3342 return nil, nil, fmt.Errorf("error converting coin: %w", err) 3343 } 3344 3345 // First check if we have the funding coin cached. If so, grab the address 3346 // from there. 3347 dcr.fundingMtx.RLock() 3348 fCoin, found := dcr.fundingCoins[op.pt] 3349 dcr.fundingMtx.RUnlock() 3350 var addr string 3351 if found { 3352 addr = fCoin.addr 3353 } else { 3354 // Check if we can get the address from wallet.UnspentOutput. 3355 // op.tree may be wire.TxTreeUnknown but wallet.UnspentOutput is 3356 // able to deal with that and find the actual tree. 3357 txOut, err := dcr.wallet.UnspentOutput(dcr.ctx, op.txHash(), op.vout(), op.tree) 3358 if err != nil { 3359 dcr.log.Errorf("gettxout error for SignMessage coin %s: %v", op, err) 3360 } else if txOut != nil { 3361 if len(txOut.Addresses) != 1 { 3362 // TODO: SignMessage is usually called for coins selected by 3363 // FundOrder. Should consider rejecting/ignoring multisig ops 3364 // in FundOrder to prevent this SignMessage error from killing 3365 // order placements. 3366 return nil, nil, fmt.Errorf("multi-sig not supported") 3367 } 3368 addr = txOut.Addresses[0] 3369 found = true 3370 } 3371 } 3372 // Could also try the gettransaction endpoint, which is supposed to return 3373 // information about wallet transactions, but which (I think?) doesn't list 3374 // ssgen outputs. 3375 if !found { 3376 return nil, nil, fmt.Errorf("did not locate coin %s. is this a coin returned from Fund?", coin) 3377 } 3378 address, err := stdaddr.DecodeAddress(addr, dcr.chainParams) 3379 if err != nil { 3380 return nil, nil, fmt.Errorf("error decoding address: %w", err) 3381 } 3382 priv, err := dcr.wallet.AddressPrivKey(dcr.ctx, address) 3383 if err != nil { 3384 return nil, nil, err 3385 } 3386 defer priv.Zero() 3387 hash := chainhash.HashB(msg) // legacy servers will not accept this signature! 3388 signature := ecdsa.Sign(priv, hash) 3389 pubkeys = append(pubkeys, priv.PubKey().SerializeCompressed()) 3390 sigs = append(sigs, signature.Serialize()) // DER format 3391 return pubkeys, sigs, nil 3392 } 3393 3394 // AuditContract retrieves information about a swap contract from the provided 3395 // txData if it represents a valid transaction that pays to the contract at the 3396 // specified coinID. The txData may be empty to attempt retrieval of the 3397 // transaction output from the network, but it is only ensured to succeed for a 3398 // full node or, if the tx is confirmed, an SPV wallet. Normally the server 3399 // should communicate this txData, and the caller can decide to require it. The 3400 // ability to work with an empty txData is a convenience for recovery tools and 3401 // testing, and it may change in the future if a GetTxData method is added for 3402 // this purpose. Optionally, attempt is also made to broadcasted the txData to 3403 // the blockchain network but it is not necessary that the broadcast succeeds 3404 // since the contract may have already been broadcasted. 3405 func (dcr *ExchangeWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroadcast bool) (*asset.AuditInfo, error) { 3406 txHash, vout, err := decodeCoinID(coinID) 3407 if err != nil { 3408 return nil, err 3409 } 3410 3411 // Get the receiving address. 3412 _, receiver, stamp, secretHash, err := dexdcr.ExtractSwapDetails(contract, dcr.chainParams) 3413 if err != nil { 3414 return nil, fmt.Errorf("error extracting swap addresses: %w", err) 3415 } 3416 3417 // If no tx data is provided, attempt to get the required data (the txOut) 3418 // from the wallet. If this is a full node wallet, a simple gettxout RPC is 3419 // sufficient with no pkScript or "since" time. If this is an SPV wallet, 3420 // only a confirmed counterparty contract can be located, and only one 3421 // within ContractSearchLimit. As such, this mode of operation is not 3422 // intended for normal server-coordinated operation. 3423 var contractTx *wire.MsgTx 3424 var contractTxOut *wire.TxOut 3425 var txTree int8 3426 if len(txData) == 0 { 3427 // Fall back to gettxout, but we won't have the tx to rebroadcast. 3428 output, err := dcr.wallet.UnspentOutput(dcr.ctx, txHash, vout, wire.TxTreeUnknown) 3429 if err == nil { 3430 contractTxOut = output.TxOut 3431 txTree = output.Tree 3432 } else { 3433 // Next, try a block filters scan. 3434 scriptAddr, err := stdaddr.NewAddressScriptHashV0(contract, dcr.chainParams) 3435 if err != nil { 3436 return nil, fmt.Errorf("error encoding script address: %w", err) 3437 } 3438 _, pkScript := scriptAddr.PaymentScript() 3439 outFound, _, err := dcr.externalTxOutput(dcr.ctx, newOutPoint(txHash, vout), 3440 pkScript, time.Now().Add(-ContractSearchLimit)) 3441 if err != nil { 3442 return nil, fmt.Errorf("error finding unspent contract: %s:%d : %w", txHash, vout, err) 3443 } 3444 contractTxOut = outFound.TxOut 3445 txTree = outFound.tree 3446 } 3447 } else { 3448 contractTx, err = msgTxFromBytes(txData) 3449 if err != nil { 3450 return nil, fmt.Errorf("invalid contract tx data: %w", err) 3451 } 3452 if err = blockchain.CheckTransactionSanity(contractTx, uint64(dcr.chainParams.MaxTxSize)); err != nil { 3453 return nil, fmt.Errorf("invalid contract tx data: %w", err) 3454 } 3455 if checkHash := contractTx.TxHash(); checkHash != *txHash { 3456 return nil, fmt.Errorf("invalid contract tx data: expected hash %s, got %s", txHash, checkHash) 3457 } 3458 if int(vout) >= len(contractTx.TxOut) { 3459 return nil, fmt.Errorf("invalid contract tx data: no output at %d", vout) 3460 } 3461 contractTxOut = contractTx.TxOut[vout] 3462 txTree = determineTxTree(contractTx) 3463 } 3464 3465 // Validate contract output. 3466 // Script must be P2SH, with 1 address and 1 required signature. 3467 scriptClass, addrs := stdscript.ExtractAddrs(contractTxOut.Version, contractTxOut.PkScript, dcr.chainParams) 3468 if scriptClass != stdscript.STScriptHash { 3469 return nil, fmt.Errorf("unexpected script class %d", scriptClass) 3470 } 3471 if len(addrs) != 1 { 3472 return nil, fmt.Errorf("unexpected number of addresses for P2SH script: %d", len(addrs)) 3473 } 3474 // Compare the contract hash to the P2SH address. 3475 contractHash := dcrutil.Hash160(contract) 3476 addr := addrs[0] 3477 addrScript, err := dexdcr.AddressScript(addr) 3478 if err != nil { 3479 return nil, err 3480 } 3481 if !bytes.Equal(contractHash, addrScript) { 3482 return nil, fmt.Errorf("contract hash doesn't match script address. %x != %x", 3483 contractHash, addrScript) 3484 } 3485 3486 // The counter-party should have broadcasted the contract tx but rebroadcast 3487 // just in case to ensure that the tx is sent to the network. Do not block 3488 // because this is not required and does not affect the audit result. 3489 if rebroadcast && contractTx != nil { 3490 go func() { 3491 if hashSent, err := dcr.wallet.SendRawTransaction(dcr.ctx, contractTx, true); err != nil { 3492 dcr.log.Debugf("Rebroadcasting counterparty contract %v (THIS MAY BE NORMAL): %v", txHash, err) 3493 } else if !hashSent.IsEqual(txHash) { 3494 dcr.log.Errorf("Counterparty contract %v was rebroadcast as %v!", txHash, hashSent) 3495 } 3496 }() 3497 } 3498 3499 return &asset.AuditInfo{ 3500 Coin: newOutput(txHash, vout, uint64(contractTxOut.Value), txTree), 3501 Contract: contract, 3502 SecretHash: secretHash, 3503 Recipient: receiver.String(), 3504 Expiration: time.Unix(int64(stamp), 0).UTC(), 3505 }, nil 3506 } 3507 3508 func determineTxTree(msgTx *wire.MsgTx) int8 { 3509 if stake.DetermineTxType(msgTx) != stake.TxTypeRegular { 3510 return wire.TxTreeStake 3511 } 3512 return wire.TxTreeRegular 3513 } 3514 3515 // lookupTxOutput attempts to find and return details for the specified output, 3516 // first checking for an unspent output and if not found, checking wallet txs. 3517 // Returns asset.CoinNotFoundError if the output is not found. 3518 // 3519 // NOTE: This method is only guaranteed to return results for outputs belonging 3520 // to transactions that are tracked by the wallet, although full node wallets 3521 // are able to look up non-wallet outputs that are unspent. 3522 // 3523 // If the value of the spent flag is -1, it could not be determined with the SPV 3524 // wallet if it is spent, and the caller should perform a block filters scan to 3525 // locate a (mined) spending transaction if needed. 3526 func (dcr *ExchangeWallet) lookupTxOutput(ctx context.Context, txHash *chainhash.Hash, vout uint32) (txOut *wire.TxOut, confs uint32, spent int8, err error) { 3527 // Check for an unspent output. 3528 output, err := dcr.wallet.UnspentOutput(ctx, txHash, vout, wire.TxTreeUnknown) 3529 if err == nil { 3530 return output.TxOut, output.Confirmations, 0, nil 3531 } else if !errors.Is(err, asset.CoinNotFoundError) { 3532 return nil, 0, 0, err 3533 } 3534 3535 // Check wallet transactions. 3536 tx, err := dcr.wallet.GetTransaction(ctx, txHash) 3537 if err != nil { 3538 return nil, 0, 0, err // asset.CoinNotFoundError if not found 3539 } 3540 if int(vout) >= len(tx.MsgTx.TxOut) { 3541 return nil, 0, 0, fmt.Errorf("tx %s has no output at %d", txHash, vout) 3542 } 3543 3544 txOut = tx.MsgTx.TxOut[vout] 3545 confs = uint32(tx.Confirmations) 3546 3547 // We have the requested output. Check if it is spent. 3548 if confs == 0 { 3549 // Only counts as spent if spent in a mined transaction, 3550 // unconfirmed tx outputs can't be spent in a mined tx. 3551 3552 // There is a dcrwallet bug by which the user can shut down at the wrong 3553 // time and a tx will never be marked as confirmed. We'll force a 3554 // cfilter scan for unconfirmed txs until the bug is resolved. 3555 // https://github.com/decred/dcrdex/pull/2444 3556 if dcr.wallet.SpvMode() { 3557 return txOut, confs, -1, nil 3558 } 3559 return txOut, confs, 0, nil 3560 } 3561 3562 if !dcr.wallet.SpvMode() { 3563 // A mined output that is not found by wallet.UnspentOutput 3564 // is spent if the wallet is connected to a full node. 3565 dcr.log.Debugf("Output %s:%d that was not reported as unspent is considered SPENT, spv mode = false.", 3566 txHash, vout) 3567 return txOut, confs, 1, nil 3568 } 3569 3570 // For SPV wallets, only consider the output spent if it pays to the wallet 3571 // because outputs that don't pay to the wallet may be unspent but still not 3572 // found by wallet.UnspentOutput. NOTE: Swap contracts never pay to wallet 3573 // (p2sh with no imported redeem script), so this is not an expected outcome 3574 // for swap contract outputs! 3575 // 3576 // for _, details := range tx.Details { 3577 // if details.Vout == vout && details.Category == wallet.CreditReceive.String() { 3578 // dcr.log.Tracef("Output %s:%d was not reported as unspent, pays to the wallet and is considered SPENT.", 3579 // txHash, vout) 3580 // return txOut, confs, 1, nil 3581 // } 3582 // } 3583 3584 // Spend status is unknown. Caller may scan block filters if needed. 3585 dcr.log.Tracef("Output %s:%d was not reported as unspent by SPV wallet. Spend status UNKNOWN.", 3586 txHash, vout) 3587 return txOut, confs, -1, nil // unknown spend status 3588 } 3589 3590 // LockTimeExpired returns true if the specified locktime has expired, making it 3591 // possible to redeem the locked coins. 3592 func (dcr *ExchangeWallet) LockTimeExpired(ctx context.Context, lockTime time.Time) (bool, error) { 3593 blockHash := dcr.cachedBestBlock().hash 3594 hdr, err := dcr.wallet.GetBlockHeader(ctx, blockHash) 3595 if err != nil { 3596 return false, fmt.Errorf("unable to retrieve the block header: %w", err) 3597 } 3598 return time.Unix(hdr.MedianTime, 0).After(lockTime), nil 3599 } 3600 3601 // ContractLockTimeExpired returns true if the specified contract's locktime has 3602 // expired, making it possible to issue a Refund. 3603 func (dcr *ExchangeWallet) ContractLockTimeExpired(ctx context.Context, contract dex.Bytes) (bool, time.Time, error) { 3604 _, _, locktime, _, err := dexdcr.ExtractSwapDetails(contract, dcr.chainParams) 3605 if err != nil { 3606 return false, time.Time{}, fmt.Errorf("error extracting contract locktime: %w", err) 3607 } 3608 contractExpiry := time.Unix(int64(locktime), 0).UTC() 3609 expired, err := dcr.LockTimeExpired(ctx, contractExpiry) 3610 if err != nil { 3611 return false, time.Time{}, err 3612 } 3613 return expired, contractExpiry, nil 3614 } 3615 3616 // FindRedemption watches for the input that spends the specified contract 3617 // coin, and returns the spending input and the contract's secret key when it 3618 // finds a spender. 3619 // 3620 // This method blocks until the redemption is found, an error occurs or the 3621 // provided context is canceled. 3622 func (dcr *ExchangeWallet) FindRedemption(ctx context.Context, coinID, _ dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) { 3623 txHash, vout, err := decodeCoinID(coinID) 3624 if err != nil { 3625 return nil, nil, fmt.Errorf("cannot decode contract coin id: %w", err) 3626 } 3627 3628 // Add this contract to the findRedemptionQueue before performing 3629 // initial redemption search (see below). The initial search done 3630 // below only checks tx inputs in mempool and blocks starting from 3631 // the block in which the contract coin is mined up till the current 3632 // best block (for mined contracts, that is). 3633 // Adding this contract to the findRedemptionQueue now makes it 3634 // possible to find the redemption if the contract is redeemed in a 3635 // later transaction. Additional redemption searches are triggered 3636 // for all contracts in the findRedemptionQueue whenever a new block 3637 // or a re-org is observed in the dcr.monitorBlocks goroutine. 3638 // This contract will be removed from the findRedemptionQueue when 3639 // the redemption is found or if the provided context is canceled 3640 // before the redemption is found. 3641 contractOutpoint := newOutPoint(txHash, vout) 3642 resultChan, contractBlock, err := dcr.queueFindRedemptionRequest(ctx, contractOutpoint) 3643 if err != nil { 3644 return nil, nil, err 3645 } 3646 3647 // Run initial search for redemption. If this contract is unmined, 3648 // only scan mempool transactions as mempool contracts can only be 3649 // spent by another mempool tx. If the contract is mined, scan all 3650 // mined tx inputs starting from the block in which the contract is 3651 // mined, up till the current best block. If the redemption is not 3652 // found in that block range, proceed to check mempool. 3653 if contractBlock == nil { 3654 dcr.findRedemptionsInMempool([]outPoint{contractOutpoint}) 3655 } else { 3656 bestBlock := dcr.cachedBestBlock() 3657 dcr.findRedemptionsInBlockRange(contractBlock.height, bestBlock.height, []outPoint{contractOutpoint}) 3658 } 3659 3660 // Wait for a find redemption result or context cancellation. 3661 // If the context is cancelled during an active mempool or block 3662 // range search, the contract will be removed from the queue and 3663 // there will be no further redemption searches for the contract. 3664 // See findRedemptionsIn{Mempool,BlockRange} -> findRedemptionsInTx. 3665 // If there is no active redemption search for this contract and 3666 // the context is canceled while waiting for new blocks to search, 3667 // the context cancellation will be caught here and the contract 3668 // will be removed from queue to prevent further searches when new 3669 // blocks are observed. 3670 var result *findRedemptionResult 3671 select { 3672 case result = <-resultChan: 3673 case <-ctx.Done(): 3674 } 3675 3676 // If this contract is still in the findRedemptionQueue, remove from the queue 3677 // to prevent further redemption search attempts for this contract. 3678 dcr.findRedemptionMtx.Lock() 3679 delete(dcr.findRedemptionQueue, contractOutpoint) 3680 dcr.findRedemptionMtx.Unlock() 3681 3682 // result would be nil if ctx is canceled or the result channel 3683 // is closed without data, which would happen if the redemption 3684 // search is aborted when this ExchangeWallet is shut down. 3685 if result != nil { 3686 return result.RedemptionCoinID, result.Secret, result.Err 3687 } 3688 return nil, nil, fmt.Errorf("aborted search for redemption of contract %s: %w", 3689 contractOutpoint, ctx.Err()) 3690 } 3691 3692 // queueFindRedemptionRequest extracts the contract hash and tx block (if mined) 3693 // of the provided contract outpoint, creates a find redemption request and adds 3694 // it to the findRedemptionQueue. Returns error if a find redemption request is 3695 // already queued for the contract or if the contract hash or block info cannot 3696 // be extracted. 3697 func (dcr *ExchangeWallet) queueFindRedemptionRequest(ctx context.Context, contractOutpoint outPoint) (chan *findRedemptionResult, *block, error) { 3698 dcr.findRedemptionMtx.Lock() 3699 defer dcr.findRedemptionMtx.Unlock() 3700 3701 if _, inQueue := dcr.findRedemptionQueue[contractOutpoint]; inQueue { 3702 return nil, nil, fmt.Errorf("duplicate find redemption request for %s", contractOutpoint.String()) 3703 } 3704 txHash, vout := contractOutpoint.txHash, contractOutpoint.vout 3705 tx, err := dcr.wallet.GetTransaction(dcr.ctx, &txHash) 3706 if err != nil { 3707 return nil, nil, err 3708 } 3709 if int(vout) > len(tx.MsgTx.TxOut)-1 { 3710 return nil, nil, fmt.Errorf("vout index %d out of range for transaction %s", vout, txHash) 3711 } 3712 contractScript := tx.MsgTx.TxOut[vout].PkScript 3713 contractScriptVer := tx.MsgTx.TxOut[vout].Version 3714 if !stdscript.IsScriptHashScript(contractScriptVer, contractScript) { 3715 return nil, nil, fmt.Errorf("coin %s not a valid contract", contractOutpoint.String()) 3716 } 3717 var contractBlock *block 3718 if tx.BlockHash != "" { 3719 blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) 3720 if err != nil { 3721 return nil, nil, fmt.Errorf("invalid blockhash %s for contract %s: %w", tx.BlockHash, contractOutpoint.String(), err) 3722 } 3723 header, err := dcr.wallet.GetBlockHeader(dcr.ctx, blockHash) 3724 if err != nil { 3725 return nil, nil, fmt.Errorf("error fetching block header %s for contract %s: %w", 3726 tx.BlockHash, contractOutpoint.String(), err) 3727 } 3728 contractBlock = &block{height: int64(header.Height), hash: blockHash} 3729 } 3730 3731 resultChan := make(chan *findRedemptionResult, 1) 3732 dcr.findRedemptionQueue[contractOutpoint] = &findRedemptionReq{ 3733 ctx: ctx, 3734 contractP2SHScript: contractScript, 3735 contractOutputScriptVer: contractScriptVer, 3736 resultChan: resultChan, 3737 } 3738 return resultChan, contractBlock, nil 3739 } 3740 3741 // findRedemptionsInMempool attempts to find spending info for the specified 3742 // contracts by searching every input of all txs in the mempool. 3743 // If spending info is found for any contract, the contract is purged from the 3744 // findRedemptionQueue and the contract's secret (if successfully parsed) or any 3745 // error that occurs during parsing is returned to the redemption finder via the 3746 // registered result chan. 3747 func (dcr *ExchangeWallet) findRedemptionsInMempool(contractOutpoints []outPoint) { 3748 contractsCount := len(contractOutpoints) 3749 dcr.log.Debugf("finding redemptions for %d contracts in mempool", contractsCount) 3750 3751 var totalFound, totalCanceled int 3752 logAbandon := func(reason string) { 3753 // Do not remove the contracts from the findRedemptionQueue 3754 // as they could be subsequently redeemed in some mined tx(s), 3755 // which would be captured when a new tip is reported. 3756 if totalFound+totalCanceled > 0 { 3757 dcr.log.Debugf("%d redemptions found, %d canceled out of %d contracts in mempool", 3758 totalFound, totalCanceled, contractsCount) 3759 } 3760 dcr.log.Errorf("abandoning mempool redemption search for %d contracts because of %s", 3761 contractsCount-totalFound-totalCanceled, reason) 3762 } 3763 3764 mempooler, is := dcr.wallet.(Mempooler) 3765 if !is || dcr.wallet.SpvMode() { 3766 return 3767 } 3768 3769 mempoolTxs, err := mempooler.GetRawMempool(dcr.ctx) 3770 if err != nil { 3771 logAbandon(fmt.Sprintf("error retrieving transactions: %v", err)) 3772 return 3773 } 3774 3775 for _, txHash := range mempoolTxs { 3776 tx, err := dcr.wallet.GetTransaction(dcr.ctx, txHash) 3777 if err != nil { 3778 logAbandon(fmt.Sprintf("getrawtransaction error for tx hash %v: %v", txHash, err)) 3779 return 3780 } 3781 found, canceled := dcr.findRedemptionsInTx("mempool", tx.MsgTx, contractOutpoints) 3782 totalFound += found 3783 totalCanceled += canceled 3784 if totalFound+totalCanceled == contractsCount { 3785 break 3786 } 3787 } 3788 3789 dcr.log.Debugf("%d redemptions found, %d canceled out of %d contracts in mempool", 3790 totalFound, totalCanceled, contractsCount) 3791 } 3792 3793 // findRedemptionsInBlockRange attempts to find spending info for the specified 3794 // contracts by checking the cfilters of each block in the provided range for 3795 // likely inclusion of ANY of the specified contracts' P2SH script. If a block's 3796 // cfilters reports possible inclusion of ANY of the contracts' P2SH script, 3797 // all inputs of the matching block's txs are checked to determine if any of the 3798 // inputs spends any of the provided contracts. 3799 // If spending info is found for any contract, the contract is purged from the 3800 // findRedemptionQueue and the contract's secret (if successfully parsed) or any 3801 // error that occurs during parsing is returned to the redemption finder via the 3802 // registered result chan. 3803 // If spending info is not found for any of these contracts after checking the 3804 // specified block range, a mempool search is triggered to attempt finding unmined 3805 // redemptions for the remaining contracts. 3806 // NOTE: 3807 // Any error encountered while checking a block's cfilters or fetching a matching 3808 // block's txs compromises the redemption search for this set of contracts because 3809 // subsequent attempts to find these contracts' redemption will not repeat any 3810 // block in the specified range unless the contracts are first removed from the 3811 // findRedemptionQueue. Thus, any such error will cause this set of contracts to 3812 // be purged from the findRedemptionQueue. The error will be propagated to the 3813 // redemption finder(s) and these may re-call dcr.FindRedemption to restart find 3814 // redemption attempts for any of these contracts. 3815 func (dcr *ExchangeWallet) findRedemptionsInBlockRange(startBlockHeight, endBlockHeight int64, contractOutpoints []outPoint) { 3816 totalContracts := len(contractOutpoints) 3817 dcr.log.Debugf("finding redemptions for %d contracts in blocks %d - %d", 3818 totalContracts, startBlockHeight, endBlockHeight) 3819 3820 var lastScannedBlockHeight int64 3821 var totalFound, totalCanceled int 3822 3823 rangeBlocks: 3824 for blockHeight := startBlockHeight; blockHeight <= endBlockHeight; blockHeight++ { 3825 // Get the hash for this block. 3826 blockHash, err := dcr.wallet.GetBlockHash(dcr.ctx, blockHeight) 3827 if err != nil { // unable to get block hash is a fatal error 3828 err = fmt.Errorf("unable to get hash for block %d: %w", blockHeight, err) 3829 dcr.fatalFindRedemptionsError(err, contractOutpoints) 3830 return 3831 } 3832 3833 // Combine the p2sh scripts for all contracts (excluding contracts whose redemption 3834 // have been found) to check against this block's cfilters. 3835 dcr.findRedemptionMtx.RLock() 3836 contractP2SHScripts := make([][]byte, 0) 3837 for _, contractOutpoint := range contractOutpoints { 3838 if req, stillInQueue := dcr.findRedemptionQueue[contractOutpoint]; stillInQueue { 3839 contractP2SHScripts = append(contractP2SHScripts, req.contractP2SHScript) 3840 } 3841 } 3842 dcr.findRedemptionMtx.RUnlock() 3843 3844 bingo, err := dcr.wallet.MatchAnyScript(dcr.ctx, blockHash, contractP2SHScripts) 3845 if err != nil { // error retrieving a block's cfilters is a fatal error 3846 err = fmt.Errorf("MatchAnyScript error for block %d (%s): %w", blockHeight, blockHash, err) 3847 dcr.fatalFindRedemptionsError(err, contractOutpoints) 3848 return 3849 } 3850 3851 if !bingo { 3852 lastScannedBlockHeight = blockHeight 3853 continue // block does not reference any of these contracts, continue to next block 3854 } 3855 3856 // Pull the block info to confirm if any of its inputs spends a contract of interest. 3857 blk, err := dcr.wallet.GetBlock(dcr.ctx, blockHash) 3858 if err != nil { // error pulling a matching block's transactions is a fatal error 3859 err = fmt.Errorf("error retrieving transactions for block %d (%s): %w", 3860 blockHeight, blockHash, err) 3861 dcr.fatalFindRedemptionsError(err, contractOutpoints) 3862 return 3863 } 3864 3865 lastScannedBlockHeight = blockHeight 3866 scanPoint := fmt.Sprintf("block %d", blockHeight) 3867 for _, tx := range append(blk.Transactions, blk.STransactions...) { 3868 found, canceled := dcr.findRedemptionsInTx(scanPoint, tx, contractOutpoints) 3869 totalFound += found 3870 totalCanceled += canceled 3871 if totalFound+totalCanceled == totalContracts { 3872 break rangeBlocks 3873 } 3874 } 3875 } 3876 3877 dcr.log.Debugf("%d redemptions found, %d canceled out of %d contracts in blocks %d to %d", 3878 totalFound, totalCanceled, totalContracts, startBlockHeight, lastScannedBlockHeight) 3879 3880 // Search for redemptions in mempool if there are yet unredeemed 3881 // contracts after searching this block range. 3882 pendingContractsCount := totalContracts - totalFound - totalCanceled 3883 if pendingContractsCount > 0 { 3884 dcr.findRedemptionMtx.RLock() 3885 pendingContracts := make([]outPoint, 0, pendingContractsCount) 3886 for _, contractOutpoint := range contractOutpoints { 3887 if _, pending := dcr.findRedemptionQueue[contractOutpoint]; pending { 3888 pendingContracts = append(pendingContracts, contractOutpoint) 3889 } 3890 } 3891 dcr.findRedemptionMtx.RUnlock() 3892 dcr.findRedemptionsInMempool(pendingContracts) 3893 } 3894 } 3895 3896 // findRedemptionsInTx checks if any input of the passed tx spends any of the 3897 // specified contract outpoints. If spending info is found for any contract, the 3898 // contract's secret or any error encountered while trying to parse the secret 3899 // is returned to the redemption finder via the registered result chan; and the 3900 // contract is purged from the findRedemptionQueue. 3901 // Returns the number of redemptions found and canceled. 3902 func (dcr *ExchangeWallet) findRedemptionsInTx(scanPoint string, tx *wire.MsgTx, contractOutpoints []outPoint) (found, cancelled int) { 3903 dcr.findRedemptionMtx.Lock() 3904 defer dcr.findRedemptionMtx.Unlock() 3905 3906 redeemTxHash := tx.TxHash() 3907 3908 for _, contractOutpoint := range contractOutpoints { 3909 req, exists := dcr.findRedemptionQueue[contractOutpoint] 3910 if !exists { 3911 continue // no find request for this outpoint (impossible now?) 3912 } 3913 if req.canceled() { 3914 cancelled++ 3915 delete(dcr.findRedemptionQueue, contractOutpoint) 3916 continue // this find request has been cancelled 3917 } 3918 3919 for i, txIn := range tx.TxIn { 3920 prevOut := &txIn.PreviousOutPoint 3921 if prevOut.Index != contractOutpoint.vout || prevOut.Hash != contractOutpoint.txHash { 3922 continue // input doesn't redeem this contract, check next input 3923 } 3924 found++ 3925 3926 scriptHash := dexdcr.ExtractScriptHash(req.contractOutputScriptVer, req.contractP2SHScript) 3927 secret, err := dexdcr.FindKeyPush(req.contractOutputScriptVer, txIn.SignatureScript, scriptHash, dcr.chainParams) 3928 if err != nil { 3929 dcr.log.Errorf("Error parsing contract secret for %s from tx input %s:%d in %s: %v", 3930 contractOutpoint.String(), redeemTxHash, i, scanPoint, err) 3931 req.resultChan <- &findRedemptionResult{ 3932 Err: err, 3933 } 3934 } else { 3935 dcr.log.Infof("Redemption for contract %s found in tx input %s:%d in %s", 3936 contractOutpoint.String(), redeemTxHash, i, scanPoint) 3937 req.resultChan <- &findRedemptionResult{ 3938 RedemptionCoinID: toCoinID(&redeemTxHash, uint32(i)), 3939 Secret: secret, 3940 } 3941 } 3942 3943 delete(dcr.findRedemptionQueue, contractOutpoint) 3944 break // stop checking inputs for this contract 3945 } 3946 } 3947 3948 return 3949 } 3950 3951 // fatalFindRedemptionsError should be called when an error occurs that prevents 3952 // redemption search for the specified contracts from continuing reliably. The 3953 // error will be propagated to the seeker(s) of these contracts' redemptions via 3954 // the registered result channels and the contracts will be removed from the 3955 // findRedemptionQueue. 3956 func (dcr *ExchangeWallet) fatalFindRedemptionsError(err error, contractOutpoints []outPoint) { 3957 dcr.findRedemptionMtx.Lock() 3958 dcr.log.Debugf("stopping redemption search for %d contracts in queue: %v", len(contractOutpoints), err) 3959 for _, contractOutpoint := range contractOutpoints { 3960 req, exists := dcr.findRedemptionQueue[contractOutpoint] 3961 if !exists { 3962 continue 3963 } 3964 req.resultChan <- &findRedemptionResult{ 3965 Err: err, 3966 } 3967 delete(dcr.findRedemptionQueue, contractOutpoint) 3968 } 3969 dcr.findRedemptionMtx.Unlock() 3970 } 3971 3972 // Refund refunds a contract. This can only be used after the time lock has 3973 // expired. This MUST return an asset.CoinNotFoundError error if the coin is 3974 // spent. If the provided fee rate is zero, an internal estimate will be used, 3975 // otherwise it will be used directly, but this behavior may change. 3976 // NOTE: The contract cannot be retrieved from the unspent coin info as the 3977 // wallet does not store it, even though it was known when the init transaction 3978 // was created. The client should store this information for persistence across 3979 // sessions. 3980 func (dcr *ExchangeWallet) Refund(coinID, contract dex.Bytes, feeRate uint64) (dex.Bytes, error) { 3981 // Caller should provide a non-zero fee rate, so we could just do 3982 // dcr.feeRateWithFallback(feeRate), but be permissive for now. 3983 if feeRate == 0 { 3984 feeRate = dcr.targetFeeRateWithFallback(2, 0) 3985 } 3986 msgTx, refundVal, fee, err := dcr.refundTx(coinID, contract, 0, nil, feeRate) 3987 if err != nil { 3988 return nil, fmt.Errorf("error creating refund tx: %w", err) 3989 } 3990 3991 refundHash, err := dcr.broadcastTx(msgTx) 3992 if err != nil { 3993 return nil, err 3994 } 3995 dcr.addTxToHistory(&asset.WalletTransaction{ 3996 Type: asset.Refund, 3997 ID: refundHash.String(), 3998 Amount: refundVal, 3999 Fees: fee, 4000 }, refundHash, true) 4001 4002 return toCoinID(refundHash, 0), nil 4003 } 4004 4005 // refundTx crates and signs a contract's refund transaction. If refundAddr is 4006 // not supplied, one will be requested from the wallet. If val is not supplied 4007 // it will be retrieved with gettxout. 4008 func (dcr *ExchangeWallet) refundTx(coinID, contract dex.Bytes, val uint64, refundAddr stdaddr.Address, feeRate uint64) (tx *wire.MsgTx, refundVal, txFee uint64, err error) { 4009 txHash, vout, err := decodeCoinID(coinID) 4010 if err != nil { 4011 return nil, 0, 0, err 4012 } 4013 // Grab the output, make sure it's unspent and get the value if not supplied. 4014 if val == 0 { 4015 utxo, _, spent, err := dcr.lookupTxOutput(dcr.ctx, txHash, vout) 4016 if err != nil { 4017 return nil, 0, 0, fmt.Errorf("error finding unspent contract: %w", err) 4018 } 4019 if utxo == nil { 4020 return nil, 0, 0, asset.CoinNotFoundError 4021 } 4022 val = uint64(utxo.Value) 4023 4024 switch spent { 4025 case 0: // unspent, proceed to create refund tx 4026 case 1, -1: // spent or unknown 4027 // Attempt to identify if it was manually refunded with the backup 4028 // transaction, in which case we can skip broadcast and record the 4029 // spending transaction we may locate as below. 4030 4031 // First find the block containing the output itself. 4032 scriptAddr, err := stdaddr.NewAddressScriptHashV0(contract, dcr.chainParams) 4033 if err != nil { 4034 return nil, 0, 0, fmt.Errorf("error encoding contract address: %w", err) 4035 } 4036 _, pkScript := scriptAddr.PaymentScript() 4037 outFound, _, err := dcr.externalTxOutput(dcr.ctx, newOutPoint(txHash, vout), 4038 pkScript, time.Now().Add(-60*24*time.Hour)) // search up to 60 days ago 4039 if err != nil { 4040 return nil, 0, 0, err // possibly the contract is still in mempool 4041 } 4042 // Try to find a transaction that spends it. 4043 spent, err := dcr.isOutputSpent(dcr.ctx, outFound) // => findTxOutSpender 4044 if err != nil { 4045 return nil, 0, 0, fmt.Errorf("error checking if contract %v:%d is spent: %w", txHash, vout, err) 4046 } 4047 if spent { 4048 spendTx := outFound.spenderTx 4049 // Refunds are not batched, so input 0 is always the spender. 4050 if dexdcr.IsRefundScript(utxo.Version, spendTx.TxIn[0].SignatureScript, contract) { 4051 return spendTx, 0, 0, nil 4052 } // otherwise it must be a redeem 4053 return nil, 0, 0, fmt.Errorf("contract %s:%d is spent in %v (%w)", 4054 txHash, vout, spendTx.TxHash(), asset.CoinNotFoundError) 4055 } 4056 } 4057 } 4058 4059 sender, _, lockTime, _, err := dexdcr.ExtractSwapDetails(contract, dcr.chainParams) 4060 if err != nil { 4061 return nil, 0, 0, fmt.Errorf("error extracting swap addresses: %w", err) 4062 } 4063 4064 // Create the transaction that spends the contract. 4065 msgTx := wire.NewMsgTx() 4066 msgTx.LockTime = uint32(lockTime) 4067 prevOut := wire.NewOutPoint(txHash, vout, wire.TxTreeRegular) 4068 txIn := wire.NewTxIn(prevOut, int64(val), []byte{}) 4069 // Enable the OP_CHECKLOCKTIMEVERIFY opcode to be used. 4070 // 4071 // https://github.com/decred/dcrd/blob/8f5270b707daaa1ecf24a1ba02b3ff8a762674d3/txscript/opcode.go#L981-L998 4072 txIn.Sequence = wire.MaxTxInSequenceNum - 1 4073 msgTx.AddTxIn(txIn) 4074 // Calculate fees and add the change output. 4075 size := msgTx.SerializeSize() + dexdcr.RefundSigScriptSize + dexdcr.P2PKHOutputSize 4076 fee := feeRate * uint64(size) 4077 if fee > val { 4078 return nil, 0, 0, fmt.Errorf("refund tx not worth the fees") 4079 } 4080 4081 if refundAddr == nil { 4082 refundAddr, err = dcr.wallet.ExternalAddress(dcr.ctx, dcr.depositAccount()) 4083 if err != nil { 4084 return nil, 0, 0, fmt.Errorf("error getting new address from the wallet: %w", err) 4085 } 4086 } 4087 pkScriptVer, pkScript := refundAddr.PaymentScript() 4088 txOut := newTxOut(int64(val-fee), pkScriptVer, pkScript) 4089 // One last check for dust. 4090 if dexdcr.IsDust(txOut, feeRate) { 4091 return nil, 0, 0, fmt.Errorf("refund output is dust") 4092 } 4093 msgTx.AddTxOut(txOut) 4094 // Sign it. 4095 refundSig, refundPubKey, err := dcr.createSig(msgTx, 0, contract, sender) 4096 if err != nil { 4097 return nil, 0, 0, err 4098 } 4099 redeemSigScript, err := dexdcr.RefundP2SHContract(contract, refundSig, refundPubKey) 4100 if err != nil { 4101 return nil, 0, 0, err 4102 } 4103 txIn.SignatureScript = redeemSigScript 4104 return msgTx, val, fee, nil 4105 } 4106 4107 // DepositAddress returns an address for depositing funds into the exchange 4108 // wallet. 4109 func (dcr *ExchangeWallet) DepositAddress() (string, error) { 4110 acct := dcr.depositAccount() 4111 addr, err := dcr.wallet.ExternalAddress(dcr.ctx, acct) 4112 if err != nil { 4113 return "", err 4114 } 4115 return addr.String(), nil 4116 } 4117 4118 // RedemptionAddress gets an address for use in redeeming the counterparty's 4119 // swap. This would be included in their swap initialization. 4120 func (dcr *ExchangeWallet) RedemptionAddress() (string, error) { 4121 return dcr.DepositAddress() 4122 } 4123 4124 // NewAddress returns a new address from the wallet. This satisfies the 4125 // NewAddresser interface. 4126 func (dcr *ExchangeWallet) NewAddress() (string, error) { 4127 return dcr.DepositAddress() 4128 } 4129 4130 // Unlock unlocks the exchange wallet. 4131 func (dcr *ExchangeWallet) Unlock(pw []byte) error { 4132 // Older SPV wallet potentially need an upgrade while we have a password. 4133 acctsToUnlock := dcr.allAccounts() 4134 if upgrader, is := dcr.wallet.(interface { 4135 upgradeAccounts(ctx context.Context, pw []byte) error 4136 }); is { 4137 if err := upgrader.upgradeAccounts(dcr.ctx, pw); err != nil { 4138 return fmt.Errorf("error upgrading accounts: %w", err) 4139 } 4140 // For the native wallet, we unlock all accounts regardless. Otherwise, 4141 // the accounts won't be properly unlocked after ConfigureFundsMixer 4142 // is called. We could consider taking a password for 4143 // ConfigureFundsMixer OR have Core take the password and call Unlock 4144 // after ConfigureFundsMixer. 4145 acctsToUnlock = nativeAccounts 4146 } 4147 4148 // We must unlock all accounts, including any unmixed account, which is used 4149 // to supply keys to the refund path of the swap contract script. 4150 for _, acct := range acctsToUnlock { 4151 unlocked, err := dcr.wallet.AccountUnlocked(dcr.ctx, acct) 4152 if err != nil { 4153 return err 4154 } 4155 if unlocked { 4156 continue // attempt to unlock the other account 4157 } 4158 4159 err = dcr.wallet.UnlockAccount(dcr.ctx, pw, acct) 4160 if err != nil { 4161 return err 4162 } 4163 } 4164 return nil 4165 } 4166 4167 // Lock locks the exchange wallet. 4168 func (dcr *ExchangeWallet) Lock() error { 4169 accts := dcr.wallet.Accounts() 4170 if accts.UnmixedAccount != "" { 4171 return fmt.Errorf("cannot lock RPC mixing wallet") // don't lock if mixing is enabled 4172 } 4173 return dcr.wallet.LockAccount(dcr.ctx, accts.PrimaryAccount) 4174 } 4175 4176 // Locked will be true if the wallet is currently locked. 4177 // Q: why are we ignoring RPC errors in this? 4178 func (dcr *ExchangeWallet) Locked() bool { 4179 for _, acct := range dcr.allAccounts() { 4180 unlocked, err := dcr.wallet.AccountUnlocked(dcr.ctx, acct) 4181 if err != nil { 4182 dcr.log.Errorf("error checking account lock status %v", err) 4183 unlocked = false // assume wallet is unlocked? 4184 } 4185 if !unlocked { 4186 return true // Locked is true if any of the funding accounts is locked. 4187 } 4188 } 4189 return false 4190 } 4191 4192 func bondPushDataScript(ver uint16, acctID []byte, lockTimeSec int64, pkh []byte) ([]byte, error) { 4193 pushData := make([]byte, 2+len(acctID)+4+20) 4194 var offset int 4195 binary.BigEndian.PutUint16(pushData[offset:], ver) 4196 offset += 2 4197 copy(pushData[offset:], acctID[:]) 4198 offset += len(acctID) 4199 binary.BigEndian.PutUint32(pushData[offset:], uint32(lockTimeSec)) 4200 offset += 4 4201 copy(pushData[offset:], pkh) 4202 return txscript.NewScriptBuilder(). 4203 AddOp(txscript.OP_RETURN). 4204 AddData(pushData). 4205 Script() 4206 } 4207 4208 // MakeBondTx creates a time-locked fidelity bond transaction. The V0 4209 // transaction has two required outputs: 4210 // 4211 // Output 0 is a the time-locked bond output of type P2SH with the provided 4212 // value. The redeem script looks similar to the refund path of an atomic swap 4213 // script, but with a pubkey hash: 4214 // 4215 // <locktime> OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 <pubkeyhash[20]> OP_EQUALVERIFY OP_CHECKSIG 4216 // 4217 // The pubkey referenced by the script is provided by the caller. 4218 // 4219 // Output 1 is a DEX Account commitment. This is an OP_RETURN output that 4220 // references the provided account ID. 4221 // 4222 // OP_RETURN <2-byte version> <32-byte account ID> <4-byte locktime> <20-byte pubkey hash> 4223 // 4224 // Having the account ID in the raw allows the txn alone to identify the account 4225 // without the bond output's redeem script. 4226 // 4227 // Output 2 is change, if any. 4228 // 4229 // The bond output's redeem script, which is needed to spend the bond output, is 4230 // returned as the Data field of the Bond. The bond output pays to a pubkeyhash 4231 // script for a wallet address. Bond.RedeemTx is a backup transaction that 4232 // spends the bond output after lockTime passes, paying to an address for the 4233 // current underlying wallet; the bond private key should normally be used to 4234 // author a new transaction paying to a new address instead. 4235 func (dcr *ExchangeWallet) MakeBondTx(ver uint16, amt, feeRate uint64, lockTime time.Time, 4236 bondKey *secp256k1.PrivateKey, acctID []byte) (*asset.Bond, func(), error) { 4237 if ver != 0 { 4238 return nil, nil, errors.New("only version 0 bonds supported") 4239 } 4240 if until := time.Until(lockTime); until >= 365*12*time.Hour /* ~6 months */ { 4241 return nil, nil, fmt.Errorf("that lock time is nuts: %v", lockTime) 4242 } else if until < 0 { 4243 return nil, nil, fmt.Errorf("that lock time is already passed: %v", lockTime) 4244 } 4245 4246 pk := bondKey.PubKey().SerializeCompressed() 4247 pkh := stdaddr.Hash160(pk) 4248 4249 feeRate = dcr.feeRateWithFallback(feeRate) 4250 baseTx := wire.NewMsgTx() 4251 const scriptVersion = 0 4252 4253 // TL output. 4254 lockTimeSec := lockTime.Unix() 4255 if lockTimeSec >= dexdcr.MaxCLTVScriptNum || lockTimeSec <= 0 { 4256 return nil, nil, fmt.Errorf("invalid lock time %v", lockTime) 4257 } 4258 bondScript, err := dexdcr.MakeBondScript(ver, uint32(lockTimeSec), pkh) 4259 if err != nil { 4260 return nil, nil, fmt.Errorf("failed to build bond output redeem script: %w", err) 4261 } 4262 bondAddr, err := stdaddr.NewAddressScriptHash(scriptVersion, bondScript, dcr.chainParams) 4263 if err != nil { 4264 return nil, nil, fmt.Errorf("failed to build bond output payment script: %w", err) 4265 } 4266 bondPkScriptVer, bondPkScript := bondAddr.PaymentScript() 4267 txOut := newTxOut(int64(amt), bondPkScriptVer, bondPkScript) 4268 if dexdcr.IsDust(txOut, feeRate) { 4269 return nil, nil, fmt.Errorf("bond output is dust") 4270 } 4271 baseTx.AddTxOut(txOut) 4272 4273 // Acct ID commitment and bond details output, v0. The integers are encoded 4274 // with big-endian byte order and a fixed number of bytes, unlike in Script, 4275 // for natural visual inspection of the version and lock time. 4276 commitPkScript, err := bondPushDataScript(ver, acctID, lockTimeSec, pkh) 4277 if err != nil { 4278 return nil, nil, fmt.Errorf("failed to build acct commit output script: %w", err) 4279 } 4280 acctOut := newTxOut(0, scriptVersion, commitPkScript) // value zero 4281 baseTx.AddTxOut(acctOut) 4282 4283 // NOTE: this "fund -> addInputCoins -> signTxAndAddChange -> lock prevouts" 4284 // sequence might be best encapsulated in a fundRawTransactionMethod. 4285 baseSize := uint32(baseTx.SerializeSize()) + dexdcr.P2PKHOutputSize // uint32(dexdcr.MsgTxOverhead + dexdcr.P2PKHOutputSize*3) 4286 enough := sendEnough(amt, feeRate, false, baseSize, true) 4287 coins, _, _, _, err := dcr.fund(0, enough) 4288 if err != nil { 4289 return nil, nil, fmt.Errorf("Unable to send %s DCR with fee rate of %d atoms/byte: %w", 4290 amount(amt), feeRate, err) 4291 } 4292 4293 var txIDToRemoveFromHistory *chainhash.Hash // will be non-nil if tx was added to history 4294 4295 abandon := func() { // if caller does not broadcast, or we fail in this method 4296 _, err := dcr.returnCoins(coins) 4297 if err != nil { 4298 dcr.log.Errorf("error returning coins for unused bond tx: %v", coins) 4299 } 4300 if txIDToRemoveFromHistory != nil { 4301 dcr.removeTxFromHistory(txIDToRemoveFromHistory) 4302 } 4303 } 4304 4305 var success bool 4306 defer func() { 4307 if !success { 4308 abandon() 4309 } 4310 }() 4311 4312 _, err = dcr.addInputCoins(baseTx, coins) 4313 if err != nil { 4314 return nil, nil, err 4315 } 4316 4317 signedTx, _, _, fee, err := dcr.signTxAndAddChange(baseTx, feeRate, -1, dcr.depositAccount()) 4318 if err != nil { 4319 return nil, nil, err 4320 } 4321 txHash := signedTx.CachedTxHash() // spentAmt := amt + fees 4322 4323 signedTxBytes, err := signedTx.Bytes() 4324 if err != nil { 4325 return nil, nil, err 4326 } 4327 unsignedTxBytes, err := baseTx.Bytes() 4328 if err != nil { 4329 return nil, nil, err 4330 } 4331 4332 // Prep the redeem / refund tx. 4333 redeemMsgTx, err := dcr.makeBondRefundTxV0(txHash, 0, amt, bondScript, bondKey, feeRate) 4334 if err != nil { 4335 return nil, nil, fmt.Errorf("unable to create bond redemption tx: %w", err) 4336 } 4337 redeemTx, err := redeemMsgTx.Bytes() 4338 if err != nil { 4339 return nil, nil, fmt.Errorf("failed to serialize bond redemption tx: %w", err) 4340 } 4341 4342 bond := &asset.Bond{ 4343 Version: ver, 4344 AssetID: BipID, 4345 Amount: amt, 4346 CoinID: toCoinID(txHash, 0), 4347 Data: bondScript, 4348 SignedTx: signedTxBytes, 4349 UnsignedTx: unsignedTxBytes, 4350 RedeemTx: redeemTx, 4351 } 4352 success = true 4353 4354 bondInfo := &asset.BondTxInfo{ 4355 AccountID: acctID, 4356 LockTime: uint64(lockTimeSec), 4357 BondID: pkh, 4358 } 4359 dcr.addTxToHistory(&asset.WalletTransaction{ 4360 Type: asset.CreateBond, 4361 ID: txHash.String(), 4362 Amount: amt, 4363 Fees: fee, 4364 BondInfo: bondInfo, 4365 }, txHash, false) 4366 4367 txIDToRemoveFromHistory = txHash 4368 4369 return bond, abandon, nil 4370 } 4371 4372 func (dcr *ExchangeWallet) makeBondRefundTxV0(txid *chainhash.Hash, vout uint32, amt uint64, 4373 script []byte, priv *secp256k1.PrivateKey, feeRate uint64) (*wire.MsgTx, error) { 4374 lockTime, pkhPush, err := dexdcr.ExtractBondDetailsV0(0, script) 4375 if err != nil { 4376 return nil, err 4377 } 4378 4379 pk := priv.PubKey().SerializeCompressed() 4380 pkh := stdaddr.Hash160(pk) 4381 if !bytes.Equal(pkh, pkhPush) { 4382 return nil, asset.ErrIncorrectBondKey 4383 } 4384 4385 redeemMsgTx := wire.NewMsgTx() 4386 // Transaction LockTime must be <= spend time, and >= the CLTV lockTime, so 4387 // we use exactly the CLTV's value. This limits the CLTV value to 32-bits. 4388 redeemMsgTx.LockTime = lockTime 4389 bondPrevOut := wire.NewOutPoint(txid, vout, wire.TxTreeRegular) 4390 txIn := wire.NewTxIn(bondPrevOut, int64(amt), []byte{}) 4391 txIn.Sequence = wire.MaxTxInSequenceNum - 1 // not finalized, do not disable cltv 4392 redeemMsgTx.AddTxIn(txIn) 4393 4394 // Calculate fees and add the refund output. 4395 redeemSize := redeemMsgTx.SerializeSize() + dexdcr.RedeemBondSigScriptSize + dexdcr.P2PKHOutputSize 4396 fee := feeRate * uint64(redeemSize) 4397 if fee > amt { 4398 return nil, fmt.Errorf("irredeemable bond at fee rate %d atoms/byte", feeRate) 4399 } 4400 4401 redeemAddr, err := dcr.wallet.ExternalAddress(dcr.ctx, dcr.wallet.Accounts().PrimaryAccount) 4402 if err != nil { 4403 return nil, fmt.Errorf("error getting new address from the wallet: %w", translateRPCCancelErr(err)) 4404 } 4405 redeemScriptVer, redeemPkScript := redeemAddr.PaymentScript() 4406 redeemTxOut := newTxOut(int64(amt-fee), redeemScriptVer, redeemPkScript) 4407 if dexdcr.IsDust(redeemTxOut, feeRate) { // hard to imagine 4408 return nil, fmt.Errorf("redeem output is dust") 4409 } 4410 redeemMsgTx.AddTxOut(redeemTxOut) 4411 4412 // CalcSignatureHash and ecdsa.Sign with secp256k1 private key. 4413 redeemInSig, err := sign.RawTxInSignature(redeemMsgTx, 0, script, txscript.SigHashAll, 4414 priv.Serialize(), dcrec.STEcdsaSecp256k1) 4415 if err != nil { 4416 return nil, fmt.Errorf("error creating signature for bond redeem input script '%v': %w", redeemAddr, err) 4417 } 4418 4419 bondRedeemSigScript, err := dexdcr.RefundBondScript(script, redeemInSig, pk) 4420 if err != nil { 4421 return nil, fmt.Errorf("failed to build bond redeem input script: %w", err) 4422 } 4423 redeemMsgTx.TxIn[0].SignatureScript = bondRedeemSigScript 4424 4425 return redeemMsgTx, nil 4426 } 4427 4428 // RefundBond refunds a bond output to a new wallet address given the redeem 4429 // script and private key. After broadcasting, the output paying to the wallet 4430 // is returned. 4431 func (dcr *ExchangeWallet) RefundBond(ctx context.Context, ver uint16, coinID, script []byte, 4432 amt uint64, privKey *secp256k1.PrivateKey) (asset.Coin, error) { 4433 if ver != 0 { 4434 return nil, errors.New("only version 0 bonds supported") 4435 } 4436 lockTime, pkhPush, err := dexdcr.ExtractBondDetailsV0(0, script) 4437 if err != nil { 4438 return nil, err 4439 } 4440 txHash, vout, err := decodeCoinID(coinID) 4441 if err != nil { 4442 return nil, err 4443 } 4444 4445 feeRate := dcr.targetFeeRateWithFallback(2, 0) 4446 4447 msgTx, err := dcr.makeBondRefundTxV0(txHash, vout, amt, script, privKey, feeRate) 4448 if err != nil { 4449 return nil, err 4450 } 4451 4452 redeemHash, err := dcr.wallet.SendRawTransaction(ctx, msgTx, false) 4453 if err != nil { // TODO: we need to be much smarter about these send error types/codes 4454 return nil, translateRPCCancelErr(err) 4455 } 4456 4457 refundAmt := msgTx.TxOut[0].Value 4458 bondInfo := &asset.BondTxInfo{ 4459 LockTime: uint64(lockTime), 4460 BondID: pkhPush, 4461 } 4462 dcr.addTxToHistory(&asset.WalletTransaction{ 4463 Type: asset.RedeemBond, 4464 ID: redeemHash.String(), 4465 Amount: amt, 4466 Fees: amt - uint64(refundAmt), 4467 BondInfo: bondInfo, 4468 }, redeemHash, true) 4469 4470 return newOutput(redeemHash, 0, uint64(refundAmt), wire.TxTreeRegular), nil 4471 4472 /* If we need to find the actual unspent bond transaction for any of: 4473 (1) the output amount, (2) the commitment output data, or (3) to ensure 4474 it is unspent, we can locate it as follows: 4475 4476 // First try without cfilters (gettxout or gettransaction). If bond was 4477 // funded by this wallet or had a change output paying to this wallet, it 4478 // should be found here. 4479 txOut, _, spent, err := dcr.lookupTxOutput(ctx, txHash, vout) 4480 if err == nil { 4481 if spent { 4482 return nil, errors.New("bond already spent") 4483 } 4484 return dcr.makeBondRefundTxV0(txHash, vout, uint64(txOut.Value), script, privKey, feeRate) 4485 } 4486 if !errors.Is(err, asset.CoinNotFoundError) { 4487 dcr.log.Warnf("Unexpected error looking up bond output %v:%d", txHash, vout) 4488 } 4489 4490 // Try block filters. This would only be required if the bond tx is foreign. 4491 // In general, the bond should have been created with this wallet. 4492 // I was hesitant to even support this, but might as well cover this edge. 4493 // NOTE: An alternative is to have the caller provide the amount, which is 4494 // all we're getting from the located tx output! 4495 scriptAddr, err := stdaddr.NewAddressScriptHashV0(script, dcr.chainParams) 4496 if err != nil { 4497 return nil, fmt.Errorf("error encoding script address: %w", err) 4498 } 4499 _, pkScript := scriptAddr.PaymentScript() 4500 outFound, _, err := dcr.externalTxOutput(dcr.ctx, newOutPoint(txHash, vout), 4501 pkScript, time.Now().Add(-365*24*time.Hour)) // long! 4502 if err != nil { 4503 return nil, err // may be asset.CoinNotFoundError 4504 } 4505 txOut = outFound.TxOut // outFound.tree 4506 spent, err = dcr.isOutputSpent(ctx, outFound) 4507 if err != nil { 4508 return nil, fmt.Errorf("error checking if output %v:%d is spent: %w", txHash, vout, err) 4509 } 4510 if spent { 4511 return nil, errors.New("bond already spent") 4512 } 4513 4514 return dcr.makeBondRefundTxV0(txHash, vout, uint64(txOut.Value), script, privKey, feeRate) 4515 */ 4516 } 4517 4518 // FindBond finds the bond with coinID and returns the values used to create it. 4519 func (dcr *ExchangeWallet) FindBond(ctx context.Context, coinID []byte, searchUntil time.Time) (bond *asset.BondDetails, err error) { 4520 txHash, vout, err := decodeCoinID(coinID) 4521 if err != nil { 4522 return nil, err 4523 } 4524 4525 decodeV0BondTx := func(msgTx *wire.MsgTx) (*asset.BondDetails, error) { 4526 if len(msgTx.TxOut) < 2 { 4527 return nil, fmt.Errorf("tx %s is not a v0 bond transaction: too few outputs", txHash) 4528 } 4529 _, lockTime, pkh, err := dexdcr.ExtractBondCommitDataV0(0, msgTx.TxOut[1].PkScript) 4530 if err != nil { 4531 return nil, fmt.Errorf("unable to extract bond commitment details from output 1 of %s: %v", txHash, err) 4532 } 4533 // Sanity check. 4534 bondScript, err := dexdcr.MakeBondScript(0, lockTime, pkh[:]) 4535 if err != nil { 4536 return nil, fmt.Errorf("failed to build bond output redeem script: %w", err) 4537 } 4538 bondAddr, err := stdaddr.NewAddressScriptHash(0, bondScript, dcr.chainParams) 4539 if err != nil { 4540 return nil, fmt.Errorf("failed to build bond output payment script: %w", err) 4541 } 4542 _, bondScriptWOpcodes := bondAddr.PaymentScript() 4543 if !bytes.Equal(bondScriptWOpcodes, msgTx.TxOut[0].PkScript) { 4544 return nil, fmt.Errorf("bond script does not match commit data for %s: %x != %x", 4545 txHash, bondScript, msgTx.TxOut[0].PkScript) 4546 } 4547 return &asset.BondDetails{ 4548 Bond: &asset.Bond{ 4549 Version: 0, 4550 AssetID: BipID, 4551 Amount: uint64(msgTx.TxOut[0].Value), 4552 CoinID: coinID, 4553 Data: bondScript, 4554 // 4555 // SignedTx and UnsignedTx not populated because this is 4556 // an already posted bond and these fields are no longer used. 4557 // SignedTx, UnsignedTx []byte 4558 // 4559 // RedeemTx cannot be populated because we do not have 4560 // the private key that only core knows. Core will need 4561 // the BondPKH to determine what the private key was. 4562 // RedeemTx []byte 4563 }, 4564 LockTime: time.Unix(int64(lockTime), 0), 4565 CheckPrivKey: func(bondKey *secp256k1.PrivateKey) bool { 4566 pk := bondKey.PubKey().SerializeCompressed() 4567 pkhB := stdaddr.Hash160(pk) 4568 return bytes.Equal(pkh[:], pkhB) 4569 }, 4570 }, nil 4571 } 4572 4573 // If the bond was funded by this wallet or had a change output paying 4574 // to this wallet, it should be found here. 4575 tx, err := dcr.wallet.GetTransaction(ctx, txHash) 4576 if err == nil { 4577 return decodeV0BondTx(tx.MsgTx) 4578 } 4579 if !errors.Is(err, asset.CoinNotFoundError) { 4580 dcr.log.Warnf("Unexpected error looking up bond output %v:%d", txHash, vout) 4581 } 4582 4583 // The bond was not funded by this wallet or had no change output when 4584 // restored from seed. This is not a problem. However, we are unable to 4585 // use filters because we don't know any output scripts. Brute force 4586 // finding the transaction. 4587 blockHash, _, err := dcr.wallet.GetBestBlock(ctx) 4588 if err != nil { 4589 return nil, fmt.Errorf("unable to get best hash: %v", err) 4590 } 4591 var ( 4592 blk *wire.MsgBlock 4593 msgTx *wire.MsgTx 4594 ) 4595 out: 4596 for { 4597 if err := ctx.Err(); err != nil { 4598 return nil, fmt.Errorf("bond search stopped: %w", err) 4599 } 4600 blk, err = dcr.wallet.GetBlock(ctx, blockHash) 4601 if err != nil { 4602 return nil, fmt.Errorf("error retrieving block %s: %w", blockHash, err) 4603 } 4604 if blk.Header.Timestamp.Before(searchUntil) { 4605 return nil, fmt.Errorf("searched blocks until %v but did not find the bond tx %s", searchUntil, txHash) 4606 } 4607 for _, tx := range blk.Transactions { 4608 if tx.TxHash() == *txHash { 4609 dcr.log.Debugf("Found mined tx %s in block %s.", txHash, blk.BlockHash()) 4610 msgTx = tx 4611 break out 4612 } 4613 } 4614 4615 if string(blk.Header.PrevBlock[:]) == "" { 4616 return nil, fmt.Errorf("did not find the bond tx %s", txHash) 4617 } 4618 4619 blockHash = &blk.Header.PrevBlock 4620 } 4621 return decodeV0BondTx(msgTx) 4622 } 4623 4624 // SendTransaction broadcasts a valid fully-signed transaction. 4625 func (dcr *ExchangeWallet) SendTransaction(rawTx []byte) ([]byte, error) { 4626 msgTx, err := msgTxFromBytes(rawTx) 4627 if err != nil { 4628 return nil, err 4629 } 4630 txHash, err := dcr.wallet.SendRawTransaction(dcr.ctx, msgTx, false) 4631 if err != nil { 4632 return nil, translateRPCCancelErr(err) 4633 } 4634 dcr.markTxAsSubmitted(txHash) 4635 return toCoinID(txHash, 0), nil 4636 } 4637 4638 // Withdraw withdraws funds to the specified address. Fees are subtracted from 4639 // the value. feeRate is in units of atoms/byte. 4640 // Withdraw satisfies asset.Withdrawer. 4641 func (dcr *ExchangeWallet) Withdraw(address string, value, feeRate uint64) (asset.Coin, error) { 4642 addr, err := stdaddr.DecodeAddress(address, dcr.chainParams) 4643 if err != nil { 4644 return nil, fmt.Errorf("invalid address: %s", address) 4645 } 4646 msgTx, sentVal, err := dcr.withdraw(addr, value, dcr.feeRateWithFallback(feeRate)) 4647 if err != nil { 4648 return nil, err 4649 } 4650 4651 selfSend, err := dcr.OwnsDepositAddress(address) 4652 if err != nil { 4653 dcr.log.Errorf("error checking if address %q is owned: %v", address, err) 4654 } 4655 txType := asset.Send 4656 if selfSend { 4657 txType = asset.SelfSend 4658 } 4659 4660 dcr.addTxToHistory(&asset.WalletTransaction{ 4661 Type: txType, 4662 ID: msgTx.CachedTxHash().String(), 4663 Amount: sentVal, 4664 Fees: value - sentVal, 4665 Recipient: &address, 4666 }, msgTx.CachedTxHash(), true) 4667 4668 return newOutput(msgTx.CachedTxHash(), 0, sentVal, wire.TxTreeRegular), nil 4669 } 4670 4671 // Send sends the exact value to the specified address. This is different from 4672 // Withdraw, which subtracts the tx fees from the amount sent. feeRate is in 4673 // units of atoms/byte. 4674 func (dcr *ExchangeWallet) Send(address string, value, feeRate uint64) (asset.Coin, error) { 4675 addr, err := stdaddr.DecodeAddress(address, dcr.chainParams) 4676 if err != nil { 4677 return nil, fmt.Errorf("invalid address: %s", address) 4678 } 4679 msgTx, sentVal, fee, err := dcr.sendToAddress(addr, value, dcr.feeRateWithFallback(feeRate)) 4680 if err != nil { 4681 return nil, err 4682 } 4683 4684 selfSend, err := dcr.OwnsDepositAddress(address) 4685 if err != nil { 4686 dcr.log.Errorf("error checking if address %q is owned: %v", address, err) 4687 } 4688 txType := asset.Send 4689 if selfSend { 4690 txType = asset.SelfSend 4691 } 4692 4693 dcr.addTxToHistory(&asset.WalletTransaction{ 4694 Type: txType, 4695 ID: msgTx.CachedTxHash().String(), 4696 Amount: sentVal, 4697 Fees: fee, 4698 Recipient: &address, 4699 }, msgTx.CachedTxHash(), true) 4700 4701 return newOutput(msgTx.CachedTxHash(), 0, sentVal, wire.TxTreeRegular), nil 4702 } 4703 4704 // ValidateSecret checks that the secret satisfies the contract. 4705 func (dcr *ExchangeWallet) ValidateSecret(secret, secretHash []byte) bool { 4706 h := sha256.Sum256(secret) 4707 return bytes.Equal(h[:], secretHash) 4708 } 4709 4710 // SwapConfirmations gets the number of confirmations and the spend status for 4711 // the specified swap. The contract and matchTime are provided so that wallets 4712 // may search for the coin using light filters. 4713 // 4714 // For a non-SPV wallet, if the swap appears spent but it cannot be located in a 4715 // block with a cfilters scan, this will return asset.CoinNotFoundError. For SPV 4716 // wallets, it is not an error if the transaction cannot be located SPV wallets 4717 // cannot see non-wallet transactions until they are mined. 4718 // 4719 // If the coin is located, but recognized as spent, no error is returned. 4720 func (dcr *ExchangeWallet) SwapConfirmations(ctx context.Context, coinID, contract dex.Bytes, matchTime time.Time) (confs uint32, spent bool, err error) { 4721 txHash, vout, err := decodeCoinID(coinID) 4722 if err != nil { 4723 return 0, false, err 4724 } 4725 4726 ctx, cancel := context.WithTimeout(ctx, confCheckTimeout) 4727 defer cancel() 4728 4729 // Check if we can find the contract onchain without using cfilters. 4730 var spendFlag int8 4731 _, confs, spendFlag, err = dcr.lookupTxOutput(ctx, txHash, vout) 4732 if err == nil { 4733 if spendFlag != -1 { 4734 return confs, spendFlag > 0, nil 4735 } // else go on to block filters scan 4736 } else if !errors.Is(err, asset.CoinNotFoundError) { 4737 return 0, false, err 4738 } 4739 4740 // Prepare the pkScript to find the contract output using block filters. 4741 scriptAddr, err := stdaddr.NewAddressScriptHashV0(contract, dcr.chainParams) 4742 if err != nil { 4743 return 0, false, fmt.Errorf("error encoding script address: %w", err) 4744 } 4745 _, p2shScript := scriptAddr.PaymentScript() 4746 4747 // Find the contract and its spend status using block filters. 4748 confs, spent, err = dcr.lookupTxOutWithBlockFilters(ctx, newOutPoint(txHash, vout), p2shScript, matchTime) 4749 // Don't trouble the caller if we're using an SPV wallet and the transaction 4750 // cannot be located. 4751 if errors.Is(err, asset.CoinNotFoundError) && dcr.wallet.SpvMode() { 4752 dcr.log.Debugf("SwapConfirmations - cfilters scan did not find %v:%d. "+ 4753 "Assuming in mempool.", txHash, vout) 4754 err = nil 4755 } 4756 return confs, spent, err 4757 } 4758 4759 // RegFeeConfirmations gets the number of confirmations for the specified 4760 // output. 4761 func (dcr *ExchangeWallet) RegFeeConfirmations(ctx context.Context, coinID dex.Bytes) (confs uint32, err error) { 4762 txHash, _, err := decodeCoinID(coinID) 4763 if err != nil { 4764 return 0, err 4765 } 4766 tx, err := dcr.wallet.GetTransaction(ctx, txHash) 4767 if err != nil { 4768 return 0, err 4769 } 4770 return uint32(tx.Confirmations), nil 4771 } 4772 4773 // addInputCoins adds inputs to the MsgTx to spend the specified outputs. 4774 func (dcr *ExchangeWallet) addInputCoins(msgTx *wire.MsgTx, coins asset.Coins) (uint64, error) { 4775 var totalIn uint64 4776 for _, coin := range coins { 4777 op, err := dcr.convertCoin(coin) 4778 if err != nil { 4779 return 0, err 4780 } 4781 if op.value == 0 { 4782 return 0, fmt.Errorf("zero-valued output detected for %s:%d", op.txHash(), op.vout()) 4783 } 4784 if op.tree == wire.TxTreeUnknown { // Set the correct prevout tree if unknown. 4785 unspentPrevOut, err := dcr.wallet.UnspentOutput(dcr.ctx, op.txHash(), op.vout(), op.tree) 4786 if err != nil { 4787 return 0, fmt.Errorf("unable to determine tree for prevout %s: %v", op.pt, err) 4788 } 4789 op.tree = unspentPrevOut.Tree 4790 } 4791 totalIn += op.value 4792 prevOut := op.wireOutPoint() 4793 txIn := wire.NewTxIn(prevOut, int64(op.value), []byte{}) 4794 msgTx.AddTxIn(txIn) 4795 } 4796 return totalIn, nil 4797 } 4798 4799 func (dcr *ExchangeWallet) shutdown() { 4800 dcr.bondReserves.Store(0) 4801 // or should it remember reserves in case we reconnect? There's a 4802 // reReserveFunds Core method for this... unclear 4803 4804 // Close all open channels for contract redemption searches 4805 // to prevent leakages and ensure goroutines that are started 4806 // to wait on these channels end gracefully. 4807 dcr.findRedemptionMtx.Lock() 4808 for contractOutpoint, req := range dcr.findRedemptionQueue { 4809 close(req.resultChan) 4810 delete(dcr.findRedemptionQueue, contractOutpoint) 4811 } 4812 dcr.findRedemptionMtx.Unlock() 4813 4814 // Disconnect the wallet. For rpc wallets, this shuts down 4815 // the rpcclient.Client. 4816 if dcr.wallet != nil { 4817 dcr.wallet.Disconnect() 4818 } 4819 } 4820 4821 // SyncStatus is information about the blockchain sync status. 4822 func (dcr *ExchangeWallet) SyncStatus() (ss *asset.SyncStatus, err error) { 4823 defer func() { 4824 var synced bool 4825 if ss != nil { 4826 synced = ss.Synced 4827 } 4828 4829 if wasSynced := dcr.previouslySynced.Swap(synced); synced && !wasSynced { 4830 tip := dcr.cachedBestBlock() 4831 go dcr.syncTxHistory(dcr.ctx, uint64(tip.height)) 4832 } 4833 }() 4834 4835 // If we have a rescan running, do different math. 4836 dcr.rescan.RLock() 4837 rescanProgress := dcr.rescan.progress 4838 dcr.rescan.RUnlock() 4839 4840 if rescanProgress != nil { 4841 height := dcr.cachedBestBlock().height 4842 if height < rescanProgress.scannedThrough { 4843 height = rescanProgress.scannedThrough 4844 } 4845 txHeight := uint64(rescanProgress.scannedThrough) 4846 return &asset.SyncStatus{ 4847 Synced: false, 4848 TargetHeight: uint64(height), 4849 StartingBlocks: dcr.startingBlocks.Load(), 4850 Blocks: uint64(height), 4851 Transactions: &txHeight, 4852 }, nil 4853 } 4854 4855 // No rescan in progress. Ask wallet. 4856 ss, err = dcr.wallet.SyncStatus(dcr.ctx) 4857 if err != nil { 4858 return nil, err 4859 } 4860 ss.StartingBlocks = dcr.startingBlocks.Load() 4861 return ss, nil 4862 } 4863 4864 // Combines the RPC type with the spending input information. 4865 type compositeUTXO struct { 4866 rpc *walletjson.ListUnspentResult 4867 input *dexdcr.SpendInfo 4868 confs int64 4869 // TODO: consider including isDexChange bool for consumer 4870 } 4871 4872 // parseUTXOs constructs and returns a list of compositeUTXOs from the provided 4873 // set of RPC utxos, including basic information required to spend each rpc utxo. 4874 // The returned list is sorted by ascending value. 4875 func (dcr *ExchangeWallet) parseUTXOs(unspents []*walletjson.ListUnspentResult) ([]*compositeUTXO, error) { 4876 utxos := make([]*compositeUTXO, 0, len(unspents)) 4877 for _, utxo := range unspents { 4878 if !utxo.Spendable { 4879 continue 4880 } 4881 scriptPK, err := hex.DecodeString(utxo.ScriptPubKey) 4882 if err != nil { 4883 return nil, fmt.Errorf("error decoding pubkey script for %s, script = %s: %w", utxo.TxID, utxo.ScriptPubKey, err) 4884 } 4885 redeemScript, err := hex.DecodeString(utxo.RedeemScript) 4886 if err != nil { 4887 return nil, fmt.Errorf("error decoding redeem script for %s, script = %s: %w", utxo.TxID, utxo.RedeemScript, err) 4888 } 4889 4890 // NOTE: listunspent does not indicate script version, so for the 4891 // purposes of our funding coins, we are going to assume 0. 4892 nfo, err := dexdcr.InputInfo(0, scriptPK, redeemScript, dcr.chainParams) 4893 if err != nil { 4894 if errors.Is(err, dex.UnsupportedScriptError) { 4895 continue 4896 } 4897 return nil, fmt.Errorf("error reading asset info: %w", err) 4898 } 4899 if nfo.ScriptType == dexdcr.ScriptUnsupported || nfo.NonStandardScript { 4900 // InputInfo sets NonStandardScript for P2SH with non-standard 4901 // redeem scripts. Don't return these since they cannot fund 4902 // arbitrary txns. 4903 continue 4904 } 4905 utxos = append(utxos, &compositeUTXO{ 4906 rpc: utxo, 4907 input: nfo, 4908 confs: utxo.Confirmations, 4909 }) 4910 } 4911 // Sort in ascending order by amount (smallest first). 4912 sort.Slice(utxos, func(i, j int) bool { return utxos[i].rpc.Amount < utxos[j].rpc.Amount }) 4913 return utxos, nil 4914 } 4915 4916 // lockedAtoms is the total value of locked outputs, as locked with LockUnspent. 4917 func (dcr *ExchangeWallet) lockedAtoms(acct string) (uint64, error) { 4918 lockedOutpoints, err := dcr.wallet.LockedOutputs(dcr.ctx, acct) 4919 if err != nil { 4920 return 0, err 4921 } 4922 var sum uint64 4923 for _, op := range lockedOutpoints { 4924 sum += toAtoms(op.Amount) 4925 } 4926 return sum, nil 4927 } 4928 4929 // convertCoin converts the asset.Coin to an output whose tree may be unknown. 4930 // Use wallet.UnspentOutput to determine the output tree where necessary. 4931 func (dcr *ExchangeWallet) convertCoin(coin asset.Coin) (*output, error) { 4932 op, _ := coin.(*output) 4933 if op != nil { 4934 return op, nil 4935 } 4936 txHash, vout, err := decodeCoinID(coin.ID()) 4937 if err != nil { 4938 return nil, err 4939 } 4940 return newOutput(txHash, vout, coin.Value(), wire.TxTreeUnknown), nil 4941 } 4942 4943 // withdraw sends the amount to the address. Fees are subtracted from the 4944 // sent value. 4945 func (dcr *ExchangeWallet) withdraw(addr stdaddr.Address, val, feeRate uint64) (*wire.MsgTx, uint64, error) { 4946 if val == 0 { 4947 return nil, 0, fmt.Errorf("cannot withdraw value = 0") 4948 } 4949 baseSize := uint32(dexdcr.MsgTxOverhead + dexdcr.P2PKHOutputSize*2) 4950 reportChange := dcr.wallet.Accounts().UnmixedAccount == "" // otherwise change goes to unmixed account 4951 enough := sendEnough(val, feeRate, true, baseSize, reportChange) 4952 reserves := dcr.bondReserves.Load() 4953 coins, _, _, _, err := dcr.fund(reserves, enough) 4954 if err != nil { 4955 return nil, 0, fmt.Errorf("unable to withdraw %s DCR to address %s with feeRate %d atoms/byte: %w", 4956 amount(val), addr, feeRate, err) 4957 } 4958 4959 msgTx, sentVal, err := dcr.sendCoins(coins, addr, nil, val, 0, feeRate, true) 4960 if err != nil { 4961 if _, retErr := dcr.returnCoins(coins); retErr != nil { 4962 dcr.log.Errorf("Failed to unlock coins: %v", retErr) 4963 } 4964 return nil, 0, err 4965 } 4966 return msgTx, sentVal, nil 4967 } 4968 4969 // sendToAddress sends an exact amount to an address. Transaction fees will be 4970 // in addition to the sent amount, and the output will be the zeroth output. 4971 // TODO: Just use the sendtoaddress rpc since dcrwallet respects locked utxos! 4972 func (dcr *ExchangeWallet) sendToAddress(addr stdaddr.Address, amt, feeRate uint64) (*wire.MsgTx, uint64, uint64, error) { 4973 baseSize := uint32(dexdcr.MsgTxOverhead + dexdcr.P2PKHOutputSize*2) // may be extra if change gets omitted (see signTxAndAddChange) 4974 reportChange := dcr.wallet.Accounts().UnmixedAccount == "" // otherwise change goes to unmixed account 4975 enough := sendEnough(amt, feeRate, false, baseSize, reportChange) 4976 reserves := dcr.bondReserves.Load() 4977 coins, _, _, _, err := dcr.fund(reserves, enough) 4978 if err != nil { 4979 return nil, 0, 0, fmt.Errorf("Unable to send %s DCR with fee rate of %d atoms/byte: %w", 4980 amount(amt), feeRate, err) 4981 } 4982 4983 msgTx, sentVal, err := dcr.sendCoins(coins, addr, nil, amt, 0, feeRate, false) 4984 if err != nil { 4985 if _, retErr := dcr.returnCoins(coins); retErr != nil { 4986 dcr.log.Errorf("Failed to unlock coins: %v", retErr) 4987 } 4988 return nil, 0, 0, err 4989 } 4990 4991 var totalOut uint64 4992 for _, txOut := range msgTx.TxOut { 4993 totalOut += uint64(txOut.Value) 4994 } 4995 var totalIn uint64 4996 for _, coin := range coins { 4997 totalIn += coin.Value() 4998 } 4999 5000 return msgTx, sentVal, totalIn - totalOut, nil 5001 } 5002 5003 // sendCoins sends the amount to the address as the zeroth output, spending the 5004 // specified coins. If subtract is true, the transaction fees will be taken from 5005 // the sent value, otherwise it will taken from the change output. If there is 5006 // change, it will be at index 1. 5007 // 5008 // An optional second output may be generated with the second address and amount 5009 // arguments, if addr2 is non-nil. Note that to omit the extra output, the 5010 // *interface* must be nil, not just the concrete type, so be cautious with 5011 // concrete address types because a nil pointer wrap into a non-nil std.Address! 5012 func (dcr *ExchangeWallet) sendCoins(coins asset.Coins, addr, addr2 stdaddr.Address, val, val2, feeRate uint64, 5013 subtract bool) (*wire.MsgTx, uint64, error) { 5014 baseTx := wire.NewMsgTx() 5015 _, err := dcr.addInputCoins(baseTx, coins) 5016 if err != nil { 5017 return nil, 0, err 5018 } 5019 payScriptVer, payScript := addr.PaymentScript() 5020 txOut := newTxOut(int64(val), payScriptVer, payScript) 5021 baseTx.AddTxOut(txOut) 5022 if addr2 != nil { 5023 payScriptVer, payScript := addr2.PaymentScript() 5024 txOut := newTxOut(int64(val2), payScriptVer, payScript) 5025 baseTx.AddTxOut(txOut) 5026 } 5027 5028 var feeSource int32 // subtract from vout 0 5029 if !subtract { 5030 feeSource = -1 // subtract from change 5031 } 5032 5033 tx, err := dcr.sendWithReturn(baseTx, feeRate, feeSource) 5034 if err != nil { 5035 return nil, 0, err 5036 } 5037 return tx, uint64(tx.TxOut[0].Value), err 5038 } 5039 5040 // sendAll sends the maximum sendable amount (total input amount minus fees) to 5041 // the provided account as a single output, spending the specified coins. 5042 func (dcr *ExchangeWallet) sendAll(coins asset.Coins, destAcct string) (*wire.MsgTx, uint64, error) { 5043 addr, err := dcr.wallet.InternalAddress(dcr.ctx, destAcct) 5044 if err != nil { 5045 return nil, 0, err 5046 } 5047 5048 baseTx := wire.NewMsgTx() 5049 totalIn, err := dcr.addInputCoins(baseTx, coins) 5050 if err != nil { 5051 return nil, 0, err 5052 } 5053 payScriptVer, payScript := addr.PaymentScript() 5054 txOut := newTxOut(int64(totalIn), payScriptVer, payScript) 5055 baseTx.AddTxOut(txOut) 5056 5057 feeRate := dcr.targetFeeRateWithFallback(2, 0) 5058 tx, err := dcr.sendWithReturn(baseTx, feeRate, 0) // subtract from vout 0 5059 return tx, uint64(txOut.Value), err 5060 } 5061 5062 // newTxOut returns a new transaction output with the given parameters. 5063 func newTxOut(amount int64, pkScriptVer uint16, pkScript []byte) *wire.TxOut { 5064 return &wire.TxOut{ 5065 Value: amount, 5066 Version: pkScriptVer, 5067 PkScript: pkScript, 5068 } 5069 } 5070 5071 // msgTxFromHex creates a wire.MsgTx by deserializing the hex transaction. 5072 func msgTxFromHex(txHex string) (*wire.MsgTx, error) { 5073 msgTx := wire.NewMsgTx() 5074 if err := msgTx.Deserialize(hex.NewDecoder(strings.NewReader(txHex))); err != nil { 5075 return nil, err 5076 } 5077 return msgTx, nil 5078 } 5079 5080 // msgTxFromBytes creates a wire.MsgTx by deserializing the transaction bytes. 5081 func msgTxFromBytes(txB []byte) (*wire.MsgTx, error) { 5082 msgTx := wire.NewMsgTx() 5083 if err := msgTx.Deserialize(bytes.NewReader(txB)); err != nil { 5084 return nil, err 5085 } 5086 return msgTx, nil 5087 } 5088 5089 func msgTxToHex(msgTx *wire.MsgTx) (string, error) { 5090 b, err := msgTx.Bytes() 5091 if err != nil { 5092 return "", err 5093 } 5094 return hex.EncodeToString(b), nil 5095 } 5096 5097 func (dcr *ExchangeWallet) makeExternalOut(acct string, val uint64) (*wire.TxOut, stdaddr.Address, error) { 5098 addr, err := dcr.wallet.ExternalAddress(dcr.ctx, acct) 5099 if err != nil { 5100 return nil, nil, fmt.Errorf("error creating change address: %w", err) 5101 } 5102 changeScriptVersion, changeScript := addr.PaymentScript() 5103 return newTxOut(int64(val), changeScriptVersion, changeScript), addr, nil 5104 } 5105 5106 func (dcr *ExchangeWallet) makeChangeOut(changeAcct string, val uint64) (*wire.TxOut, stdaddr.Address, error) { 5107 changeAddr, err := dcr.wallet.InternalAddress(dcr.ctx, changeAcct) 5108 if err != nil { 5109 return nil, nil, fmt.Errorf("error creating change address: %w", err) 5110 } 5111 changeScriptVersion, changeScript := changeAddr.PaymentScript() 5112 return newTxOut(int64(val), changeScriptVersion, changeScript), changeAddr, nil 5113 } 5114 5115 // sendWithReturn sends the unsigned transaction, adding a change output unless 5116 // the amount is dust. subtractFrom indicates the output from which fees should 5117 // be subtracted, where -1 indicates fees should come out of a change output. 5118 func (dcr *ExchangeWallet) sendWithReturn(baseTx *wire.MsgTx, feeRate uint64, subtractFrom int32) (*wire.MsgTx, error) { 5119 signedTx, _, _, _, err := dcr.signTxAndAddChange(baseTx, feeRate, subtractFrom, dcr.depositAccount()) 5120 if err != nil { 5121 return nil, err 5122 } 5123 5124 _, err = dcr.broadcastTx(signedTx) 5125 return signedTx, err 5126 } 5127 5128 // signTxAndAddChange signs the passed msgTx, adding a change output that pays 5129 // an address from the specified changeAcct, unless the change amount is dust. 5130 // subtractFrom indicates the output from which fees should be subtracted, where 5131 // -1 indicates fees should come out of a change output. baseTx may be modified 5132 // with an added change output or a reduced value of the subtractFrom output. 5133 func (dcr *ExchangeWallet) signTxAndAddChange(baseTx *wire.MsgTx, feeRate uint64, 5134 subtractFrom int32, changeAcct string) (*wire.MsgTx, *output, string, uint64, error) { 5135 // Sign the transaction to get an initial size estimate and calculate 5136 // whether a change output would be dust. 5137 sigCycles := 1 5138 msgTx, err := dcr.wallet.SignRawTransaction(dcr.ctx, baseTx) 5139 if err != nil { 5140 return nil, nil, "", 0, err 5141 } 5142 5143 totalIn, totalOut, remaining, _, size := reduceMsgTx(msgTx) 5144 if totalIn < totalOut { 5145 return nil, nil, "", 0, fmt.Errorf("unbalanced transaction") 5146 } 5147 5148 minFee := feeRate * size 5149 if subtractFrom == -1 && minFee > remaining { 5150 return nil, nil, "", 0, fmt.Errorf("not enough funds to cover minimum fee rate of %v atoms/B: %s > %s remaining", 5151 feeRate, amount(minFee), amount(remaining)) 5152 } 5153 if int(subtractFrom) >= len(baseTx.TxOut) { 5154 return nil, nil, "", 0, fmt.Errorf("invalid subtractFrom output %d for tx with %d outputs", 5155 subtractFrom, len(baseTx.TxOut)) 5156 } 5157 5158 // Add a change output if there is enough remaining. 5159 var changeAdded bool 5160 var changeAddress stdaddr.Address 5161 var changeOutput *wire.TxOut 5162 minFeeWithChange := (size + dexdcr.P2PKHOutputSize) * feeRate 5163 if remaining > minFeeWithChange { 5164 changeValue := remaining - minFeeWithChange 5165 if subtractFrom >= 0 { 5166 // Subtract the additional fee needed for the added change output 5167 // from the specified existing output. 5168 changeValue = remaining 5169 } 5170 if !dexdcr.IsDustVal(dexdcr.P2PKHOutputSize, changeValue, feeRate) { 5171 if subtractFrom >= 0 { // only subtract after dust check 5172 baseTx.TxOut[subtractFrom].Value -= int64(minFeeWithChange) 5173 remaining += minFeeWithChange 5174 } 5175 changeOutput, changeAddress, err = dcr.makeChangeOut(changeAcct, changeValue) 5176 if err != nil { 5177 return nil, nil, "", 0, err 5178 } 5179 dcr.log.Debugf("Change output size = %d, addr = %s", changeOutput.SerializeSize(), changeAddress.String()) 5180 changeAdded = true 5181 baseTx.AddTxOut(changeOutput) // unsigned txn 5182 remaining -= changeValue 5183 } 5184 } 5185 5186 lastFee := remaining 5187 5188 // If change added or subtracting from an existing output, iterate on fees. 5189 if changeAdded || subtractFrom >= 0 { 5190 subtractee := changeOutput 5191 if subtractFrom >= 0 { 5192 subtractee = baseTx.TxOut[subtractFrom] 5193 } 5194 // The amount available for fees is the sum of what is presently 5195 // allocated to fees (lastFee) and the value of the subtractee output, 5196 // which add to fees or absorb excess fees from lastFee. 5197 reservoir := lastFee + uint64(subtractee.Value) 5198 5199 // Find the best fee rate by closing in on it in a loop. 5200 tried := map[uint64]bool{} 5201 for { 5202 // Each cycle, sign the transaction and see if there is a need to 5203 // raise or lower the fees. 5204 sigCycles++ 5205 msgTx, err = dcr.wallet.SignRawTransaction(dcr.ctx, baseTx) 5206 if err != nil { 5207 return nil, nil, "", 0, err 5208 } 5209 size = uint64(msgTx.SerializeSize()) 5210 reqFee := feeRate * size 5211 if reqFee > reservoir { 5212 // IsDustVal check must be bugged. 5213 dcr.log.Errorf("reached the impossible place. in = %.8f, out = %.8f, reqFee = %.8f, lastFee = %.8f, raw tx = %x", 5214 toDCR(totalIn), toDCR(totalOut), toDCR(reqFee), toDCR(lastFee), dcr.wireBytes(msgTx)) 5215 return nil, nil, "", 0, fmt.Errorf("change error") 5216 } 5217 5218 // If 1) lastFee == reqFee, nothing changed since the last cycle. 5219 // And there is likely no room for improvement. If 2) The reqFee 5220 // required for a transaction of this size is less than the 5221 // currently signed transaction fees, but we've already tried it, 5222 // then it must have a larger serialize size, so the current fee is 5223 // as good as it gets. 5224 if lastFee == reqFee || (lastFee > reqFee && tried[reqFee]) { 5225 break 5226 } 5227 5228 // The minimum fee for a transaction of this size is either higher or 5229 // lower than the fee in the currently signed transaction, and it hasn't 5230 // been tried yet, so try it now. 5231 tried[lastFee] = true 5232 subtractee.Value = int64(reservoir - reqFee) // next 5233 lastFee = reqFee 5234 if dexdcr.IsDust(subtractee, feeRate) { 5235 // Another condition that should be impossible, but check anyway in case 5236 // the maximum fee was underestimated causing the first check to be 5237 // missed. 5238 dcr.log.Errorf("reached the impossible place. in = %.8f, out = %.8f, reqFee = %.8f, lastFee = %.8f, raw tx = %x", 5239 toDCR(totalIn), toDCR(totalOut), toDCR(reqFee), toDCR(lastFee), dcr.wireBytes(msgTx)) 5240 return nil, nil, "", 0, fmt.Errorf("dust error") 5241 } 5242 continue 5243 } 5244 } 5245 5246 // Double check the resulting txns fee and fee rate. 5247 _, _, checkFee, checkRate, size := reduceMsgTx(msgTx) 5248 if checkFee != lastFee { 5249 return nil, nil, "", 0, fmt.Errorf("fee mismatch! %.8f != %.8f, raw tx: %x", toDCR(checkFee), toDCR(lastFee), dcr.wireBytes(msgTx)) 5250 } 5251 // Ensure the effective fee rate is at least the required fee rate. 5252 if checkRate < feeRate { 5253 return nil, nil, "", 0, fmt.Errorf("final fee rate for %s, %d, is lower than expected, %d. raw tx: %x", 5254 msgTx.CachedTxHash(), checkRate, feeRate, dcr.wireBytes(msgTx)) 5255 } 5256 // This is a last ditch effort to catch ridiculously high fees. Right now, 5257 // it's just erroring for fees more than triple the expected rate, which is 5258 // admittedly un-scientific. This should account for any signature length 5259 // related variation as well as a potential dust change output with no 5260 // subtractee specified, in which case the dust goes to the miner. 5261 if changeAdded && checkRate > feeRate*3 { 5262 return nil, nil, "", 0, fmt.Errorf("final fee rate for %s, %d, is seemingly outrageous, target = %d, raw tx = %x", 5263 msgTx.CachedTxHash(), checkRate, feeRate, dcr.wireBytes(msgTx)) 5264 } 5265 5266 txHash := msgTx.TxHash() 5267 dcr.log.Debugf("%d signature cycles to converge on fees for tx %s: "+ 5268 "min rate = %d, actual fee rate = %d (%v for %v bytes), change = %v", 5269 sigCycles, txHash, feeRate, checkRate, checkFee, size, changeAdded) 5270 5271 var change *output 5272 var changeAddr string 5273 if changeAdded { 5274 change = newOutput(&txHash, uint32(len(msgTx.TxOut)-1), uint64(changeOutput.Value), wire.TxTreeRegular) 5275 changeAddr = changeAddress.String() 5276 } 5277 5278 return msgTx, change, changeAddr, lastFee, nil 5279 } 5280 5281 // ValidateAddress checks that the provided address is valid. 5282 func (dcr *ExchangeWallet) ValidateAddress(address string) bool { 5283 _, err := stdaddr.DecodeAddress(address, dcr.chainParams) 5284 return err == nil 5285 } 5286 5287 // dummyP2PKHScript only has to be a valid 25-byte pay-to-pubkey-hash pkScript 5288 // for EstimateSendTxFee when an empty or invalid address is provided. 5289 var dummyP2PKHScript = []byte{0x76, 0xa9, 0x14, 0xe4, 0x28, 0x61, 0xa, 5290 0xfc, 0xd0, 0x4e, 0x21, 0x94, 0xf7, 0xe2, 0xcc, 0xf8, 5291 0x58, 0x7a, 0xc9, 0xe7, 0x2c, 0x79, 0x7b, 0x88, 0xac, 5292 } 5293 5294 // EstimateSendTxFee returns a tx fee estimate for sending or withdrawing the 5295 // provided amount using the provided feeRate. 5296 func (dcr *ExchangeWallet) EstimateSendTxFee(address string, sendAmount, feeRate uint64, subtract, _ bool) (fee uint64, isValidAddress bool, err error) { 5297 if sendAmount == 0 { 5298 return 0, false, fmt.Errorf("cannot check fee: send amount = 0") 5299 } 5300 5301 feeRate = dcr.feeRateWithFallback(feeRate) 5302 5303 var pkScript []byte 5304 var payScriptVer uint16 5305 if addr, err := stdaddr.DecodeAddress(address, dcr.chainParams); err == nil { 5306 payScriptVer, pkScript = addr.PaymentScript() 5307 isValidAddress = true 5308 } else { 5309 // use a dummy 25-byte p2pkh script 5310 pkScript = dummyP2PKHScript 5311 } 5312 5313 tx := wire.NewMsgTx() 5314 5315 tx.AddTxOut(newTxOut(int64(sendAmount), payScriptVer, pkScript)) // payScriptVer is default zero 5316 5317 utxos, err := dcr.spendableUTXOs() 5318 if err != nil { 5319 return 0, false, err 5320 } 5321 5322 minTxSize := uint32(tx.SerializeSize()) 5323 reportChange := dcr.wallet.Accounts().UnmixedAccount == "" 5324 enough := sendEnough(sendAmount, feeRate, subtract, minTxSize, reportChange) 5325 sum, extra, inputsSize, _, _, _, err := tryFund(utxos, enough) 5326 if err != nil { 5327 return 0, false, err 5328 } 5329 5330 reserves := dcr.bondReserves.Load() 5331 avail := sumUTXOs(utxos) 5332 if avail-sum+extra /* avail-sendAmount-fees */ < reserves { 5333 return 0, false, errors.New("violates reserves") 5334 } 5335 5336 txSize := uint64(minTxSize + inputsSize) 5337 estFee := txSize * feeRate 5338 remaining := sum - sendAmount 5339 5340 // Check if there will be a change output if there is enough remaining. 5341 estFeeWithChange := (txSize + dexdcr.P2PKHOutputSize) * feeRate 5342 var changeValue uint64 5343 if remaining > estFeeWithChange { 5344 changeValue = remaining - estFeeWithChange 5345 } 5346 5347 if subtract { 5348 // fees are already included in sendAmount, anything else is change. 5349 changeValue = remaining 5350 } 5351 5352 var finalFee uint64 5353 if dexdcr.IsDustVal(dexdcr.P2PKHOutputSize, changeValue, feeRate) { 5354 // remaining cannot cover a non-dust change and the fee for the change. 5355 finalFee = estFee + remaining 5356 } else { 5357 // additional fee will be paid for non-dust change 5358 finalFee = estFeeWithChange 5359 } 5360 return finalFee, isValidAddress, nil 5361 } 5362 5363 // StandardSendFees returns the fees for a simple send tx with one input and two 5364 // outputs. 5365 func (dcr *ExchangeWallet) StandardSendFee(feeRate uint64) uint64 { 5366 var baseSize uint64 = dexdcr.MsgTxOverhead + dexdcr.P2PKHOutputSize*2 + dexdcr.P2PKHInputSize 5367 return feeRate * baseSize 5368 } 5369 5370 func (dcr *ExchangeWallet) isNative() bool { 5371 return dcr.walletType == walletTypeSPV 5372 } 5373 5374 // currentAgendas gets the most recent agendas from the chain params. The caller 5375 // must populate the CurrentChoice field of the agendas. 5376 func currentAgendas(chainParams *chaincfg.Params) (agendas []*asset.TBAgenda) { 5377 var bestID uint32 5378 for deploymentID := range chainParams.Deployments { 5379 if bestID == 0 || deploymentID > bestID { 5380 bestID = deploymentID 5381 } 5382 } 5383 for _, deployment := range chainParams.Deployments[bestID] { 5384 v := deployment.Vote 5385 agenda := &asset.TBAgenda{ 5386 ID: v.Id, 5387 Description: v.Description, 5388 } 5389 for _, choice := range v.Choices { 5390 agenda.Choices = append(agenda.Choices, &asset.TBChoice{ 5391 ID: choice.Id, 5392 Description: choice.Description, 5393 }) 5394 } 5395 agendas = append(agendas, agenda) 5396 } 5397 return 5398 } 5399 5400 func (dcr *ExchangeWallet) StakeStatus() (*asset.TicketStakingStatus, error) { 5401 if !dcr.connected.Load() { 5402 return nil, errors.New("not connected, login first") 5403 } 5404 // Try to get tickets first, because this will error for older RPC + SPV 5405 // wallets. 5406 tickets, err := dcr.tickets(dcr.ctx) 5407 if err != nil { 5408 if errors.Is(err, oldSPVWalletErr) { 5409 return nil, nil 5410 } 5411 return nil, fmt.Errorf("error retrieving tickets: %w", err) 5412 } 5413 sinfo, err := dcr.wallet.StakeInfo(dcr.ctx) 5414 if err != nil { 5415 return nil, err 5416 } 5417 5418 isRPC := !dcr.isNative() 5419 var vspURL string 5420 if !isRPC { 5421 if v := dcr.vspV.Load(); v != nil { 5422 vspURL = v.(*vsp).URL 5423 } 5424 } else { 5425 rpcW, ok := dcr.wallet.(*rpcWallet) 5426 if !ok { 5427 return nil, errors.New("wallet not an *rpcWallet") 5428 } 5429 walletInfo, err := rpcW.walletInfo(dcr.ctx) 5430 if err != nil { 5431 return nil, fmt.Errorf("error retrieving wallet info: %w", err) 5432 } 5433 vspURL = walletInfo.VSP 5434 } 5435 voteChoices, tSpends, treasuryPolicy, err := dcr.wallet.VotingPreferences(dcr.ctx) 5436 if err != nil { 5437 return nil, fmt.Errorf("error retrieving stances: %w", err) 5438 } 5439 agendas := currentAgendas(dcr.chainParams) 5440 for _, agenda := range agendas { 5441 for _, c := range voteChoices { 5442 if c.AgendaID == agenda.ID { 5443 agenda.CurrentChoice = c.ChoiceID 5444 break 5445 } 5446 } 5447 } 5448 5449 return &asset.TicketStakingStatus{ 5450 TicketPrice: uint64(sinfo.Sdiff), 5451 VotingSubsidy: dcr.voteSubsidy(dcr.cachedBestBlock().height), 5452 VSP: vspURL, 5453 IsRPC: isRPC, 5454 Tickets: tickets, 5455 Stances: asset.Stances{ 5456 Agendas: agendas, 5457 TreasurySpends: tSpends, 5458 TreasuryKeys: treasuryPolicy, 5459 }, 5460 Stats: dcr.ticketStatsFromStakeInfo(sinfo), 5461 }, nil 5462 } 5463 5464 func (dcr *ExchangeWallet) ticketStatsFromStakeInfo(sinfo *wallet.StakeInfoData) asset.TicketStats { 5465 return asset.TicketStats{ 5466 TotalRewards: uint64(sinfo.TotalSubsidy), 5467 TicketCount: sinfo.OwnMempoolTix + sinfo.Unspent + sinfo.Immature + sinfo.Voted + sinfo.Revoked, 5468 Votes: sinfo.Voted, 5469 Revokes: sinfo.Revoked, 5470 Mempool: sinfo.OwnMempoolTix, 5471 Queued: uint32(dcr.ticketBuyer.remaining.Load()), 5472 } 5473 } 5474 5475 func (dcr *ExchangeWallet) voteSubsidy(tipHeight int64) uint64 { 5476 // Chance of a given ticket voting in a block is 5477 // p = chainParams.TicketsPerBlock / (chainParams.TicketPoolSize * chainParams.TicketsPerBlock) 5478 // = 1 / chainParams.TicketPoolSize 5479 // Expected number of blocks to vote is 5480 // 1 / p = chainParams.TicketPoolSize 5481 expectedBlocksToVote := int64(dcr.chainParams.TicketPoolSize) 5482 voteHeightExpectationValue := tipHeight + expectedBlocksToVote 5483 return uint64(dcr.subsidyCache.CalcStakeVoteSubsidyV3(voteHeightExpectationValue, blockchain.SSVDCP0012)) 5484 } 5485 5486 // tickets gets tickets from the wallet and changes the status of "unspent" 5487 // tickets that haven't reached expiration "live". 5488 // DRAFT NOTE: From dcrwallet: 5489 // 5490 // TicketStatusUnspent is a matured ticket that has not been spent. It 5491 // is only used under SPV mode where it is unknown if a ticket is live, 5492 // was missed, or expired. 5493 // 5494 // But if the ticket has not reached a certain number of confirmations, we 5495 // can say for sure it's not expired. With auto-revocations, "missed" or 5496 // "expired" tickets are actually "revoked", I think. 5497 // The only thing I can't figure out is how SPV wallets set the spender in the 5498 // case of an auto-revocation. It might be happening here 5499 // https://github.com/decred/dcrwallet/blob/a87fa843495ec57c1d3b478c2ceb3876c3749af5/wallet/chainntfns.go#L770-L775 5500 // If we're seeing auto-revocations, we're fine to make the changes in this 5501 // method. 5502 func (dcr *ExchangeWallet) tickets(ctx context.Context) ([]*asset.Ticket, error) { 5503 tickets, err := dcr.wallet.Tickets(ctx) 5504 if err != nil { 5505 return nil, fmt.Errorf("error retrieving tickets: %w", err) 5506 } 5507 // Adjust status for SPV tickets that aren't expired. 5508 oldestTicketsBlock := dcr.cachedBestBlock().height - int64(dcr.chainParams.TicketExpiry) - int64(dcr.chainParams.TicketMaturity) 5509 for _, t := range tickets { 5510 if t.Status != asset.TicketStatusUnspent { 5511 continue 5512 } 5513 if t.Tx.BlockHeight == -1 || t.Tx.BlockHeight > oldestTicketsBlock { 5514 t.Status = asset.TicketStatusLive 5515 } 5516 } 5517 return tickets, nil 5518 } 5519 5520 func vspInfo(ctx context.Context, uri string) (*vspdjson.VspInfoResponse, error) { 5521 suffix := "/api/v3/vspinfo" 5522 path, err := neturl.JoinPath(uri, suffix) 5523 if err != nil { 5524 return nil, err 5525 } 5526 var info vspdjson.VspInfoResponse 5527 return &info, dexnet.Get(ctx, path, &info) 5528 } 5529 5530 // SetVSP sets the VSP provider. Ability to set can be checked with StakeStatus 5531 // first. Only non-RPC (internal) wallets can be set. Part of the 5532 // asset.TicketBuyer interface. 5533 func (dcr *ExchangeWallet) SetVSP(url string) error { 5534 if !dcr.isNative() { 5535 return errors.New("cannot set vsp for external wallet") 5536 } 5537 info, err := vspInfo(dcr.ctx, url) 5538 if err != nil { 5539 return err 5540 } 5541 v := vsp{ 5542 URL: url, 5543 PubKey: base64.StdEncoding.EncodeToString(info.PubKey), 5544 FeePercentage: info.FeePercentage, 5545 } 5546 b, err := json.Marshal(&v) 5547 if err != nil { 5548 return err 5549 } 5550 if err := os.WriteFile(dcr.vspFilepath, b, 0666); err != nil { 5551 return err 5552 } 5553 dcr.vspV.Store(&v) 5554 return nil 5555 } 5556 5557 // PurchaseTickets purchases n number of tickets. Part of the asset.TicketBuyer 5558 // interface. 5559 func (dcr *ExchangeWallet) PurchaseTickets(n int, feeSuggestion uint64) error { 5560 if n < 1 { 5561 return nil 5562 } 5563 if !dcr.connected.Load() { 5564 return errors.New("not connected, login first") 5565 } 5566 bal, err := dcr.Balance() 5567 if err != nil { 5568 return fmt.Errorf("error getting balance: %v", err) 5569 } 5570 isRPC := !dcr.isNative() 5571 if isRPC { 5572 rpcW, ok := dcr.wallet.(*rpcWallet) 5573 if !ok { 5574 return errors.New("wallet not an *rpcWallet") 5575 } 5576 walletInfo, err := rpcW.walletInfo(dcr.ctx) 5577 if err != nil { 5578 return fmt.Errorf("error retrieving wallet info: %w", err) 5579 } 5580 if walletInfo.SPV && walletInfo.VSP == "" { 5581 return errors.New("a vsp must best set to purchase tickets with an spv wallet") 5582 } 5583 } 5584 sinfo, err := dcr.wallet.StakeInfo(dcr.ctx) 5585 if err != nil { 5586 return fmt.Errorf("stakeinfo error: %v", err) 5587 } 5588 // I think we need to set this, otherwise we probably end up with default 5589 // of DefaultRelayFeePerKb = 1e4 => 10 atoms/byte. 5590 feePerKB := dcrutil.Amount(dcr.feeRateWithFallback(feeSuggestion) * 1000) 5591 if err := dcr.wallet.SetTxFee(dcr.ctx, feePerKB); err != nil { 5592 return fmt.Errorf("error setting wallet tx fee: %w", err) 5593 } 5594 5595 // Get a minimum size assuming a single-input split tx. 5596 fees := feePerKB * minVSPTicketPurchaseSize / 1000 5597 ticketPrice := sinfo.Sdiff + fees 5598 total := uint64(n) * uint64(ticketPrice) 5599 if bal.Available < total { 5600 return fmt.Errorf("available balance %s is lower than projected cost %s for %d tickets", 5601 dcrutil.Amount(bal.Available), dcrutil.Amount(total), n) 5602 } 5603 remain := dcr.ticketBuyer.remaining.Add(int32(n)) 5604 dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Remaining: uint32(remain)}) 5605 go dcr.runTicketBuyer() 5606 5607 return nil 5608 } 5609 5610 const ticketDataRoute = "ticketPurchaseUpdate" 5611 5612 // TicketPurchaseUpdate is an update from the asynchronous ticket purchasing 5613 // loop. 5614 type TicketPurchaseUpdate struct { 5615 Err string `json:"err,omitempty"` 5616 Remaining uint32 `json:"remaining"` 5617 Tickets []*asset.Ticket `json:"tickets"` 5618 Stats *asset.TicketStats `json:"stats,omitempty"` 5619 } 5620 5621 // runTicketBuyer attempts to buy requested tickets. Because of a dcrwallet bug, 5622 // its possible that (Wallet).PurchaseTickets will purchase fewer tickets than 5623 // requested, without error. To work around this bug, we add requested tickets 5624 // to ExchangeWallet.ticketBuyer.remaining, and re-run runTicketBuyer every 5625 // block. 5626 func (dcr *ExchangeWallet) runTicketBuyer() { 5627 tb := &dcr.ticketBuyer 5628 if !tb.running.CompareAndSwap(false, true) { 5629 // already running 5630 return 5631 } 5632 defer tb.running.Store(false) 5633 var ok bool 5634 defer func() { 5635 if !ok { 5636 tb.remaining.Store(0) 5637 } 5638 }() 5639 5640 if tb.unconfirmedTickets == nil { 5641 tb.unconfirmedTickets = make(map[chainhash.Hash]struct{}) 5642 } 5643 5644 remain := tb.remaining.Load() 5645 if remain < 1 { 5646 return 5647 } 5648 5649 for txHash := range tb.unconfirmedTickets { 5650 tx, err := dcr.wallet.GetTransaction(dcr.ctx, &txHash) 5651 if err != nil { 5652 dcr.log.Errorf("GetTransaction error ticket tx %s: %v", txHash, err) 5653 dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Err: err.Error()}) 5654 return 5655 } 5656 if tx.Confirmations > 0 { 5657 delete(tb.unconfirmedTickets, txHash) 5658 } 5659 } 5660 if len(tb.unconfirmedTickets) > 0 { 5661 ok = true 5662 dcr.log.Tracef("Skipping ticket purchase attempt because there are still %d unconfirmed tickets", len(tb.unconfirmedTickets)) 5663 return 5664 } 5665 5666 dcr.log.Tracef("Attempting to purchase %d tickets", remain) 5667 5668 bal, err := dcr.Balance() 5669 if err != nil { 5670 dcr.log.Errorf("GetBalance error: %v", err) 5671 dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Err: err.Error()}) 5672 return 5673 } 5674 sinfo, err := dcr.wallet.StakeInfo(dcr.ctx) 5675 if err != nil { 5676 dcr.log.Errorf("StakeInfo error: %v", err) 5677 dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Err: err.Error()}) 5678 return 5679 } 5680 if dcrutil.Amount(bal.Available) < sinfo.Sdiff*dcrutil.Amount(remain) { 5681 dcr.log.Errorf("Insufficient balance %s to purchase %d ticket at price %s: %v", dcrutil.Amount(bal.Available), remain, sinfo.Sdiff, err) 5682 dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Err: "insufficient balance"}) 5683 return 5684 } 5685 5686 var tickets []*asset.Ticket 5687 if !dcr.isNative() { 5688 tickets, err = dcr.wallet.PurchaseTickets(dcr.ctx, int(remain), "", "", false) 5689 } else { 5690 v := dcr.vspV.Load() 5691 if v == nil { 5692 err = errors.New("no vsp set") 5693 } else { 5694 vInfo := v.(*vsp) 5695 tickets, err = dcr.wallet.PurchaseTickets(dcr.ctx, int(remain), vInfo.URL, vInfo.PubKey, dcr.mixing.Load()) 5696 } 5697 } 5698 if err != nil { 5699 dcr.log.Errorf("PurchaseTickets error: %v", err) 5700 dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Err: err.Error()}) 5701 return 5702 } 5703 purchased := int32(len(tickets)) 5704 remain = tb.remaining.Add(-purchased) 5705 // sanity check 5706 if remain < 0 { 5707 remain = 0 5708 tb.remaining.Store(remain) 5709 } 5710 stats := dcr.ticketStatsFromStakeInfo(sinfo) 5711 stats.Mempool += uint32(len(tickets)) 5712 stats.Queued = uint32(remain) 5713 dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{ 5714 Tickets: tickets, 5715 Remaining: uint32(remain), 5716 Stats: &stats, 5717 }) 5718 for _, ticket := range tickets { 5719 txHash, err := chainhash.NewHashFromStr(ticket.Tx.Hash) 5720 if err != nil { 5721 dcr.log.Errorf("NewHashFromStr error for ticket hash %s: %v", ticket.Tx.Hash, err) 5722 dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Err: err.Error()}) 5723 return 5724 } 5725 tb.unconfirmedTickets[*txHash] = struct{}{} 5726 dcr.addTxToHistory(&asset.WalletTransaction{ 5727 Type: asset.TicketPurchase, 5728 ID: txHash.String(), 5729 Amount: ticket.Tx.TicketPrice, 5730 Fees: ticket.Tx.Fees, 5731 }, txHash, true) 5732 } 5733 ok = true 5734 } 5735 5736 // SetVotingPreferences sets the vote choices for all active tickets and future 5737 // tickets. Nil maps can be provided for no change. Part of the 5738 // asset.TicketBuyer interface. 5739 func (dcr *ExchangeWallet) SetVotingPreferences(choices map[string]string, tspendPolicy map[string]string, treasuryPolicy map[string]string) error { 5740 if !dcr.connected.Load() { 5741 return errors.New("not connected, login first") 5742 } 5743 return dcr.wallet.SetVotingPreferences(dcr.ctx, choices, tspendPolicy, treasuryPolicy) 5744 } 5745 5746 // ListVSPs lists known available voting service providers. 5747 func (dcr *ExchangeWallet) ListVSPs() ([]*asset.VotingServiceProvider, error) { 5748 if dcr.network == dex.Simnet { 5749 const simnetVSPUrl = "http://127.0.0.1:19591" 5750 vspi, err := vspInfo(dcr.ctx, simnetVSPUrl) 5751 if err != nil { 5752 dcr.log.Warnf("Error getting simnet VSP info: %v", err) 5753 return []*asset.VotingServiceProvider{}, nil 5754 } 5755 return []*asset.VotingServiceProvider{{ 5756 URL: simnetVSPUrl, 5757 Network: dex.Simnet, 5758 Launched: uint64(time.Now().Add(-time.Hour * 24 * 180).UnixMilli()), 5759 LastUpdated: uint64(time.Now().Add(-time.Minute * 15).UnixMilli()), 5760 APIVersions: vspi.APIVersions, 5761 FeePercentage: vspi.FeePercentage, 5762 Closed: vspi.VspClosed, 5763 Voting: vspi.Voting, 5764 Voted: vspi.Voted, 5765 Revoked: vspi.Revoked, 5766 VSPDVersion: vspi.VspdVersion, 5767 BlockHeight: vspi.BlockHeight, 5768 NetShare: vspi.NetworkProportion, 5769 }}, nil 5770 } 5771 5772 // This struct is not quite compatible with vspdjson.VspInfoResponse. 5773 var res map[string]*struct { 5774 Network string `json:"network"` 5775 Launched uint64 `json:"launched"` // seconds 5776 LastUpdated uint64 `json:"lastupdated"` // seconds 5777 APIVersions []int64 `json:"apiversions"` 5778 FeePercentage float64 `json:"feepercentage"` 5779 Closed bool `json:"closed"` 5780 Voting int64 `json:"voting"` 5781 Voted int64 `json:"voted"` 5782 Revoked int64 `json:"revoked"` 5783 VSPDVersion string `json:"vspdversion"` 5784 BlockHeight uint32 `json:"blockheight"` 5785 NetShare float32 `json:"estimatednetworkproportion"` 5786 } 5787 if err := dexnet.Get(dcr.ctx, "https://api.decred.org/?c=vsp", &res); err != nil { 5788 return nil, err 5789 } 5790 vspds := make([]*asset.VotingServiceProvider, 0) 5791 for host, v := range res { 5792 net, err := dex.NetFromString(v.Network) 5793 if err != nil { 5794 dcr.log.Warnf("error parsing VSP network from %q", v.Network) 5795 } 5796 if net != dcr.network { 5797 continue 5798 } 5799 vspds = append(vspds, &asset.VotingServiceProvider{ 5800 URL: "https://" + host, 5801 Network: net, 5802 Launched: v.Launched * 1000, // to milliseconds 5803 LastUpdated: v.LastUpdated * 1000, // to milliseconds 5804 APIVersions: v.APIVersions, 5805 FeePercentage: v.FeePercentage, 5806 Closed: v.Closed, 5807 Voting: v.Voting, 5808 Voted: v.Voted, 5809 Revoked: v.Revoked, 5810 VSPDVersion: v.VSPDVersion, 5811 BlockHeight: v.BlockHeight, 5812 NetShare: v.NetShare, 5813 }) 5814 } 5815 return vspds, nil 5816 } 5817 5818 // TicketPage fetches a page of tickets within a range of block numbers with a 5819 // target page size and optional offset. scanStart is the block in which to 5820 // start the scan. The scan progresses in reverse block number order, starting 5821 // at scanStart and going to progressively lower blocks. scanStart can be set to 5822 // -1 to indicate the current chain tip. 5823 func (dcr *ExchangeWallet) TicketPage(scanStart int32, n, skipN int) ([]*asset.Ticket, error) { 5824 if !dcr.connected.Load() { 5825 return nil, errors.New("not connected, login first") 5826 } 5827 pager, is := dcr.wallet.(ticketPager) 5828 if !is { 5829 return nil, errors.New("ticket pagination not supported for this wallet") 5830 } 5831 return pager.TicketPage(dcr.ctx, scanStart, n, skipN) 5832 } 5833 5834 func (dcr *ExchangeWallet) broadcastTx(signedTx *wire.MsgTx) (*chainhash.Hash, error) { 5835 txHash, err := dcr.wallet.SendRawTransaction(dcr.ctx, signedTx, false) 5836 if err != nil { 5837 return nil, fmt.Errorf("sendrawtx error: %w, raw tx: %x", err, dcr.wireBytes(signedTx)) 5838 } 5839 checkHash := signedTx.TxHash() 5840 if *txHash != checkHash { 5841 return nil, fmt.Errorf("transaction sent, but received unexpected transaction ID back from RPC server. "+ 5842 "expected %s, got %s, raw tx: %x", *txHash, checkHash, dcr.wireBytes(signedTx)) 5843 } 5844 return txHash, nil 5845 } 5846 5847 // createSig creates and returns the serialized raw signature and compressed 5848 // pubkey for a transaction input signature. 5849 func (dcr *ExchangeWallet) createSig(tx *wire.MsgTx, idx int, pkScript []byte, addr stdaddr.Address) (sig, pubkey []byte, err error) { 5850 sigType, err := dexdcr.AddressSigType(addr) 5851 if err != nil { 5852 return nil, nil, err 5853 } 5854 5855 priv, err := dcr.wallet.AddressPrivKey(dcr.ctx, addr) 5856 if err != nil { 5857 return nil, nil, err 5858 } 5859 defer priv.Zero() 5860 5861 sig, err = sign.RawTxInSignature(tx, idx, pkScript, txscript.SigHashAll, priv.Serialize(), sigType) 5862 if err != nil { 5863 return nil, nil, err 5864 } 5865 5866 return sig, priv.PubKey().SerializeCompressed(), nil 5867 } 5868 5869 func (dcr *ExchangeWallet) txDB() *btc.BadgerTxDB { 5870 db := dcr.txHistoryDB.Load() 5871 if db == nil { 5872 return nil 5873 } 5874 return db.(*btc.BadgerTxDB) 5875 } 5876 5877 func rpcTxFee(tx *ListTransactionsResult) uint64 { 5878 if tx.Fee != nil { 5879 if *tx.Fee < 0 { 5880 return toAtoms(-*tx.Fee) 5881 } 5882 return toAtoms(*tx.Fee) 5883 } 5884 return 0 5885 } 5886 5887 func isRegularMix(tx *wire.MsgTx) (isMix bool, mixDenom int64) { 5888 if len(tx.TxOut) < 3 || len(tx.TxIn) < 3 { 5889 return false, 0 5890 } 5891 5892 mixedOuts := make(map[int64]uint32) 5893 for _, o := range tx.TxOut { 5894 val := o.Value 5895 if _, ok := splitPointMap[val]; ok { 5896 mixedOuts[val]++ 5897 continue 5898 } 5899 } 5900 5901 var mixCount uint32 5902 for val, count := range mixedOuts { 5903 if count < 3 { 5904 continue 5905 } 5906 if val > mixDenom { 5907 mixDenom = val 5908 mixCount = count 5909 } 5910 } 5911 5912 // TODO: revisit the input count requirements 5913 isMix = mixCount >= uint32(len(tx.TxOut)/2) 5914 return 5915 } 5916 5917 // isMixedSplitTx tests if a transaction is a CSPP-mixed ticket split 5918 // transaction (the transaction that creates appropriately-sized outputs to be 5919 // spent by a ticket purchase). This dumbly checks for at least three outputs 5920 // of the same size and three of other sizes. It could be smarter by checking 5921 // for ticket price + ticket fee outputs, but it's impossible to know the fee 5922 // after the fact although it`s probably the default fee. 5923 func isMixedSplitTx(tx *wire.MsgTx) (isMix bool, tikPrice int64) { 5924 if len(tx.TxOut) < 6 || len(tx.TxIn) < 3 { 5925 return false, 0 5926 } 5927 values := make(map[int64]int) 5928 for _, o := range tx.TxOut { 5929 values[o.Value]++ 5930 } 5931 5932 var numPossibleTickets int 5933 for k, v := range values { 5934 if v > numPossibleTickets { 5935 numPossibleTickets = v 5936 tikPrice = k 5937 } 5938 } 5939 numOtherOut := len(tx.TxOut) - numPossibleTickets 5940 5941 // NOTE: The numOtherOut requirement may be too strict, 5942 if numPossibleTickets < 3 || numOtherOut < 3 { 5943 return false, 0 5944 } 5945 5946 return true, tikPrice 5947 } 5948 5949 func isMixTx(tx *wire.MsgTx) (isMix bool, mixDenom int64) { 5950 if isMix, mixDenom = isRegularMix(tx); isMix { 5951 return 5952 } 5953 5954 return isMixedSplitTx(tx) 5955 } 5956 5957 // idUnknownTx identifies the type and details of a transaction either made 5958 // or received by the wallet. 5959 func (dcr *ExchangeWallet) idUnknownTx(ctx context.Context, ltxr *ListTransactionsResult) (*asset.WalletTransaction, error) { 5960 txHash, err := chainhash.NewHashFromStr(ltxr.TxID) 5961 if err != nil { 5962 return nil, fmt.Errorf("error decoding tx hash %s: %v", ltxr.TxID, err) 5963 } 5964 tx, err := dcr.wallet.GetTransaction(ctx, txHash) 5965 if err != nil { 5966 return nil, fmt.Errorf("GetTransaction error: %v", err) 5967 } 5968 5969 var totalIn uint64 5970 for _, txIn := range tx.MsgTx.TxIn { 5971 if txIn.ValueIn > 0 { 5972 totalIn += uint64(txIn.ValueIn) 5973 } 5974 } 5975 5976 var totalOut uint64 5977 for _, txOut := range tx.MsgTx.TxOut { 5978 totalOut += uint64(txOut.Value) 5979 } 5980 5981 fee := rpcTxFee(ltxr) 5982 if fee == 0 && totalIn > totalOut { 5983 fee = totalIn - totalOut 5984 } 5985 5986 switch *ltxr.TxType { 5987 case walletjson.LTTTVote: 5988 return &asset.WalletTransaction{ 5989 Type: asset.TicketVote, 5990 ID: ltxr.TxID, 5991 Amount: totalOut, 5992 Fees: fee, 5993 }, nil 5994 case walletjson.LTTTRevocation: 5995 return &asset.WalletTransaction{ 5996 Type: asset.TicketRevocation, 5997 ID: ltxr.TxID, 5998 Amount: totalOut, 5999 Fees: fee, 6000 }, nil 6001 case walletjson.LTTTTicket: 6002 return &asset.WalletTransaction{ 6003 Type: asset.TicketPurchase, 6004 ID: ltxr.TxID, 6005 Amount: totalOut, 6006 Fees: fee, 6007 }, nil 6008 } 6009 6010 txIsBond := func(msgTx *wire.MsgTx) (bool, *asset.BondTxInfo) { 6011 if len(msgTx.TxOut) < 2 { 6012 return false, nil 6013 } 6014 const scriptVer = 0 6015 acctID, lockTime, pkHash, err := dexdcr.ExtractBondCommitDataV0(scriptVer, msgTx.TxOut[1].PkScript) 6016 if err != nil { 6017 return false, nil 6018 } 6019 return true, &asset.BondTxInfo{ 6020 AccountID: acctID[:], 6021 LockTime: uint64(lockTime), 6022 BondID: pkHash[:], 6023 } 6024 } 6025 if isBond, bondInfo := txIsBond(tx.MsgTx); isBond { 6026 return &asset.WalletTransaction{ 6027 Type: asset.CreateBond, 6028 ID: ltxr.TxID, 6029 Amount: uint64(tx.MsgTx.TxOut[0].Value), 6030 Fees: fee, 6031 BondInfo: bondInfo, 6032 }, nil 6033 } 6034 6035 // Any other P2SH may be a swap or a send. We cannot determine unless we 6036 // look up the transaction that spends this UTXO. 6037 txPaysToScriptHash := func(msgTx *wire.MsgTx) (v uint64) { 6038 for _, txOut := range msgTx.TxOut { 6039 if txscript.IsPayToScriptHash(txOut.PkScript) { 6040 v += uint64(txOut.Value) 6041 } 6042 } 6043 return 6044 } 6045 if v := txPaysToScriptHash(tx.MsgTx); v > 0 { 6046 return &asset.WalletTransaction{ 6047 Type: asset.SwapOrSend, 6048 ID: ltxr.TxID, 6049 Amount: v, 6050 Fees: fee, 6051 }, nil 6052 } 6053 6054 // Helper function will help us identify inputs that spend P2SH contracts. 6055 containsContractAtPushIndex := func(msgTx *wire.MsgTx, idx int, isContract func(contract []byte) bool) bool { 6056 txinloop: 6057 for _, txIn := range msgTx.TxIn { 6058 // not segwit 6059 const scriptVer = 0 6060 tokenizer := txscript.MakeScriptTokenizer(scriptVer, txIn.SignatureScript) 6061 for i := 0; i <= idx; i++ { // contract is 5th item item in redemption and 4th in refund 6062 if !tokenizer.Next() { 6063 continue txinloop 6064 } 6065 } 6066 if isContract(tokenizer.Data()) { 6067 return true 6068 } 6069 } 6070 return false 6071 } 6072 6073 // Swap redemptions and refunds 6074 contractIsSwap := func(contract []byte) bool { 6075 _, _, _, _, err := dexdcr.ExtractSwapDetails(contract, dcr.chainParams) 6076 return err == nil 6077 } 6078 redeemsSwap := func(msgTx *wire.MsgTx) bool { 6079 return containsContractAtPushIndex(msgTx, 4, contractIsSwap) 6080 } 6081 if redeemsSwap(tx.MsgTx) { 6082 return &asset.WalletTransaction{ 6083 Type: asset.Redeem, 6084 ID: ltxr.TxID, 6085 Amount: totalOut + fee, 6086 Fees: fee, 6087 }, nil 6088 } 6089 refundsSwap := func(msgTx *wire.MsgTx) bool { 6090 return containsContractAtPushIndex(msgTx, 3, contractIsSwap) 6091 } 6092 if refundsSwap(tx.MsgTx) { 6093 return &asset.WalletTransaction{ 6094 Type: asset.Refund, 6095 ID: ltxr.TxID, 6096 Amount: totalOut + fee, 6097 Fees: fee, 6098 }, nil 6099 } 6100 6101 // Bond refunds 6102 redeemsBond := func(msgTx *wire.MsgTx) (bool, *asset.BondTxInfo) { 6103 var bondInfo *asset.BondTxInfo 6104 isBond := func(contract []byte) bool { 6105 const scriptVer = 0 6106 lockTime, pkHash, err := dexdcr.ExtractBondDetailsV0(scriptVer, contract) 6107 if err != nil { 6108 return false 6109 } 6110 bondInfo = &asset.BondTxInfo{ 6111 AccountID: []byte{}, // Could look for the bond tx to get this, I guess. 6112 LockTime: uint64(lockTime), 6113 BondID: pkHash[:], 6114 } 6115 return true 6116 } 6117 return containsContractAtPushIndex(msgTx, 2, isBond), bondInfo 6118 } 6119 if isBondRedemption, bondInfo := redeemsBond(tx.MsgTx); isBondRedemption { 6120 return &asset.WalletTransaction{ 6121 Type: asset.RedeemBond, 6122 ID: ltxr.TxID, 6123 Amount: totalOut, 6124 Fees: fee, 6125 BondInfo: bondInfo, 6126 }, nil 6127 } 6128 6129 const scriptVersion = 0 6130 6131 allOutputsPayUs := func(msgTx *wire.MsgTx) bool { 6132 for _, txOut := range msgTx.TxOut { 6133 _, addrs := stdscript.ExtractAddrs(scriptVersion, txOut.PkScript, dcr.chainParams) 6134 if len(addrs) != 1 { // sanity check 6135 return false 6136 } 6137 6138 addr := addrs[0] 6139 owns, err := dcr.wallet.WalletOwnsAddress(ctx, addr) 6140 if err != nil { 6141 dcr.log.Errorf("walletOwnsAddress error: %w", err) 6142 return false 6143 } 6144 if !owns { 6145 return false 6146 } 6147 } 6148 6149 return true 6150 } 6151 6152 if ltxr.Send && allOutputsPayUs(tx.MsgTx) && len(tx.MsgTx.TxIn) == 1 { 6153 return &asset.WalletTransaction{ 6154 Type: asset.Split, 6155 ID: ltxr.TxID, 6156 Fees: fee, 6157 }, nil 6158 } 6159 6160 if isMix, mixDenom := isMixTx(tx.MsgTx); isMix { 6161 var mixedAmount uint64 6162 for _, txOut := range tx.MsgTx.TxOut { 6163 if txOut.Value == mixDenom { 6164 _, addrs := stdscript.ExtractAddrs(scriptVersion, txOut.PkScript, dcr.chainParams) 6165 if err != nil { 6166 dcr.log.Errorf("ExtractAddrs error: %w", err) 6167 continue 6168 } 6169 if len(addrs) != 1 { // sanity check 6170 continue 6171 } 6172 6173 addr := addrs[0] 6174 owns, err := dcr.wallet.WalletOwnsAddress(ctx, addr) 6175 if err != nil { 6176 dcr.log.Errorf("walletOwnsAddress error: %w", err) 6177 continue 6178 } 6179 6180 if owns { 6181 mixedAmount += uint64(txOut.Value) 6182 } 6183 } 6184 } 6185 6186 return &asset.WalletTransaction{ 6187 Type: asset.Mix, 6188 ID: ltxr.TxID, 6189 Amount: mixedAmount, 6190 Fees: fee, 6191 }, nil 6192 } 6193 6194 getRecipient := func(msgTx *wire.MsgTx, receive bool) *string { 6195 for _, txOut := range msgTx.TxOut { 6196 _, addrs := stdscript.ExtractAddrs(scriptVersion, txOut.PkScript, dcr.chainParams) 6197 if err != nil { 6198 dcr.log.Errorf("ExtractAddrs error: %w", err) 6199 continue 6200 } 6201 if len(addrs) != 1 { // sanity check 6202 continue 6203 } 6204 6205 addr := addrs[0] 6206 owns, err := dcr.wallet.WalletOwnsAddress(ctx, addr) 6207 if err != nil { 6208 dcr.log.Errorf("walletOwnsAddress error: %w", err) 6209 continue 6210 } 6211 6212 if receive == owns { 6213 str := addr.String() 6214 return &str 6215 } 6216 } 6217 return nil 6218 } 6219 6220 txOutDirection := func(msgTx *wire.MsgTx) (in, out uint64) { 6221 for _, txOut := range msgTx.TxOut { 6222 _, addrs := stdscript.ExtractAddrs(scriptVersion, txOut.PkScript, dcr.chainParams) 6223 if err != nil { 6224 dcr.log.Errorf("ExtractAddrs error: %w", err) 6225 continue 6226 } 6227 if len(addrs) != 1 { // sanity check 6228 continue 6229 } 6230 6231 addr := addrs[0] 6232 owns, err := dcr.wallet.WalletOwnsAddress(ctx, addr) 6233 if err != nil { 6234 dcr.log.Errorf("walletOwnsAddress error: %w", err) 6235 continue 6236 } 6237 if owns { 6238 in += uint64(txOut.Value) 6239 } else { 6240 out += uint64(txOut.Value) 6241 } 6242 } 6243 return 6244 } 6245 6246 in, out := txOutDirection(tx.MsgTx) 6247 6248 if ltxr.Send { 6249 txType := asset.Send 6250 amt := out 6251 if allOutputsPayUs(tx.MsgTx) { 6252 txType = asset.SelfSend 6253 amt = in 6254 } 6255 return &asset.WalletTransaction{ 6256 Type: txType, 6257 ID: ltxr.TxID, 6258 Amount: amt, 6259 Fees: fee, 6260 Recipient: getRecipient(tx.MsgTx, false), 6261 }, nil 6262 } 6263 6264 return &asset.WalletTransaction{ 6265 Type: asset.Receive, 6266 ID: ltxr.TxID, 6267 Amount: in, 6268 Fees: fee, 6269 Recipient: getRecipient(tx.MsgTx, true), 6270 }, nil 6271 } 6272 6273 // addUnknownTransactionsToHistory checks for any transactions the wallet has 6274 // made or received that are not part of the transaction history. It scans 6275 // from the last point to which it had previously scanned to the current tip. 6276 func (dcr *ExchangeWallet) addUnknownTransactionsToHistory(tip uint64) { 6277 txHistoryDB := dcr.txDB() 6278 6279 const blockQueryBuffer = 3 6280 var blockToQuery uint64 6281 lastQuery := dcr.receiveTxLastQuery.Load() 6282 if lastQuery == 0 { 6283 // TODO: use wallet birthday instead of block 0. 6284 // blockToQuery = 0 6285 } else if lastQuery < tip-blockQueryBuffer { 6286 blockToQuery = lastQuery - blockQueryBuffer 6287 } else { 6288 blockToQuery = tip - blockQueryBuffer 6289 } 6290 6291 txs, err := dcr.wallet.ListSinceBlock(dcr.ctx, int32(blockToQuery)) 6292 if err != nil { 6293 dcr.log.Errorf("Error listing transactions since block %d: %v", blockToQuery, err) 6294 return 6295 } 6296 6297 for _, tx := range txs { 6298 if dcr.ctx.Err() != nil { 6299 return 6300 } 6301 txHash, err := chainhash.NewHashFromStr(tx.TxID) 6302 if err != nil { 6303 dcr.log.Errorf("Error decoding tx hash %s: %v", tx.TxID, err) 6304 continue 6305 } 6306 _, err = txHistoryDB.GetTx(txHash.String()) 6307 if err == nil { 6308 continue 6309 } 6310 if !errors.Is(err, asset.CoinNotFoundError) { 6311 dcr.log.Errorf("Error getting tx %s: %v", txHash.String(), err) 6312 continue 6313 } 6314 wt, err := dcr.idUnknownTx(dcr.ctx, &tx) 6315 if err != nil { 6316 dcr.log.Errorf("error identifying transaction: %v", err) 6317 continue 6318 } 6319 6320 if tx.BlockIndex != nil && *tx.BlockIndex > 0 && *tx.BlockIndex < int64(tip-blockQueryBuffer) { 6321 wt.BlockNumber = uint64(*tx.BlockIndex) 6322 wt.Timestamp = uint64(tx.BlockTime) 6323 } 6324 6325 // Don't send notifications for the initial sync to avoid spamming the 6326 // front end. A notification is sent at the end of the initial sync. 6327 dcr.addTxToHistory(wt, txHash, true, blockToQuery == 0) 6328 } 6329 6330 dcr.receiveTxLastQuery.Store(tip) 6331 err = txHistoryDB.SetLastReceiveTxQuery(tip) 6332 if err != nil { 6333 dcr.log.Errorf("Error setting last query to %d: %v", tip, err) 6334 } 6335 6336 if blockToQuery == 0 { 6337 dcr.emit.TransactionHistorySyncedNote() 6338 } 6339 } 6340 6341 // syncTxHistory checks to see if there are any transactions which the wallet 6342 // has made or received that are not part of the transaction history, then 6343 // identifies and adds them. It also checks all the pending transactions to see 6344 // if they have been mined into a block, and if so, updates the transaction 6345 // history to reflect the block height. 6346 func (dcr *ExchangeWallet) syncTxHistory(ctx context.Context, tip uint64) { 6347 if !dcr.syncingTxHistory.CompareAndSwap(false, true) { 6348 return 6349 } 6350 defer dcr.syncingTxHistory.Store(false) 6351 6352 txHistoryDB := dcr.txDB() 6353 if txHistoryDB == nil { 6354 return 6355 } 6356 6357 ss, err := dcr.SyncStatus() 6358 if err != nil { 6359 dcr.log.Errorf("Error getting sync status: %v", err) 6360 return 6361 } 6362 if !ss.Synced { 6363 return 6364 } 6365 6366 dcr.addUnknownTransactionsToHistory(tip) 6367 6368 pendingTxsCopy := make(map[chainhash.Hash]btc.ExtendedWalletTx, len(dcr.pendingTxs)) 6369 dcr.pendingTxsMtx.RLock() 6370 for hash, tx := range dcr.pendingTxs { 6371 pendingTxsCopy[hash] = *tx 6372 } 6373 dcr.pendingTxsMtx.RUnlock() 6374 6375 handlePendingTx := func(txHash chainhash.Hash, tx *btc.ExtendedWalletTx) { 6376 if !tx.Submitted { 6377 return 6378 } 6379 6380 gtr, err := dcr.wallet.GetTransaction(ctx, &txHash) 6381 if errors.Is(err, asset.CoinNotFoundError) { 6382 err = txHistoryDB.RemoveTx(txHash.String()) 6383 if err == nil { 6384 dcr.pendingTxsMtx.Lock() 6385 delete(dcr.pendingTxs, txHash) 6386 dcr.pendingTxsMtx.Unlock() 6387 } else { 6388 // Leave it in the pendingPendingTxs and attempt to remove it 6389 // again next time. 6390 dcr.log.Errorf("Error removing tx %s from the history store: %v", txHash.String(), err) 6391 } 6392 return 6393 } 6394 if err != nil { 6395 dcr.log.Errorf("Error getting transaction %s: %v", txHash, err) 6396 return 6397 } 6398 6399 var updated bool 6400 if gtr.BlockHash != "" { 6401 blockHash, err := chainhash.NewHashFromStr(gtr.BlockHash) 6402 if err != nil { 6403 dcr.log.Errorf("Error decoding block hash %s: %v", gtr.BlockHash, err) 6404 return 6405 } 6406 block, err := dcr.wallet.GetBlockHeader(ctx, blockHash) 6407 if err != nil { 6408 dcr.log.Errorf("Error getting block height for %s: %v", blockHash, err) 6409 return 6410 } 6411 blockHeight := block.Height 6412 if tx.BlockNumber != uint64(blockHeight) || tx.Timestamp != uint64(block.Timestamp.Unix()) { 6413 tx.BlockNumber = uint64(blockHeight) 6414 tx.Timestamp = uint64(block.Timestamp.Unix()) 6415 updated = true 6416 } 6417 } else if gtr.BlockHash == "" && tx.BlockNumber != 0 { 6418 tx.BlockNumber = 0 6419 tx.Timestamp = 0 6420 updated = true 6421 } 6422 6423 var confs uint64 6424 if tx.BlockNumber > 0 && tip >= tx.BlockNumber { 6425 confs = tip - tx.BlockNumber + 1 6426 } 6427 if confs >= defaultRedeemConfTarget { 6428 tx.Confirmed = true 6429 updated = true 6430 } 6431 6432 if updated { 6433 err = txHistoryDB.StoreTx(tx) 6434 if err != nil { 6435 dcr.log.Errorf("Error updating tx %s: %v", txHash, err) 6436 return 6437 } 6438 6439 dcr.pendingTxsMtx.Lock() 6440 if tx.Confirmed { 6441 delete(dcr.pendingTxs, txHash) 6442 } else { 6443 dcr.pendingTxs[txHash] = tx 6444 } 6445 dcr.pendingTxsMtx.Unlock() 6446 6447 dcr.emit.TransactionNote(tx.WalletTransaction, false) 6448 } 6449 } 6450 6451 for hash, tx := range pendingTxsCopy { 6452 if dcr.ctx.Err() != nil { 6453 return 6454 } 6455 handlePendingTx(hash, &tx) 6456 } 6457 } 6458 6459 func (dcr *ExchangeWallet) markTxAsSubmitted(txHash *chainhash.Hash) { 6460 txHistoryDB := dcr.txDB() 6461 if txHistoryDB == nil { 6462 return 6463 } 6464 6465 err := txHistoryDB.MarkTxAsSubmitted(txHash.String()) 6466 if err != nil { 6467 dcr.log.Errorf("failed to mark tx as submitted in tx history db: %v", err) 6468 } 6469 6470 dcr.pendingTxsMtx.Lock() 6471 wt, found := dcr.pendingTxs[*txHash] 6472 dcr.pendingTxsMtx.Unlock() 6473 6474 if !found { 6475 dcr.log.Errorf("Transaction %s not found in pending txs", txHash) 6476 return 6477 } 6478 6479 wt.Submitted = true 6480 6481 dcr.emit.TransactionNote(wt.WalletTransaction, true) 6482 } 6483 6484 func (dcr *ExchangeWallet) removeTxFromHistory(txHash *chainhash.Hash) { 6485 txHistoryDB := dcr.txDB() 6486 if txHistoryDB == nil { 6487 return 6488 } 6489 6490 dcr.pendingTxsMtx.Lock() 6491 delete(dcr.pendingTxs, *txHash) 6492 dcr.pendingTxsMtx.Unlock() 6493 6494 err := txHistoryDB.RemoveTx(txHash.String()) 6495 if err != nil { 6496 dcr.log.Errorf("failed to remove tx from tx history db: %v", err) 6497 } 6498 } 6499 6500 func (dcr *ExchangeWallet) addTxToHistory(wt *asset.WalletTransaction, txHash *chainhash.Hash, submitted bool, skipNotes ...bool) { 6501 txHistoryDB := dcr.txDB() 6502 if txHistoryDB == nil { 6503 return 6504 } 6505 6506 ewt := &btc.ExtendedWalletTx{ 6507 WalletTransaction: wt, 6508 Submitted: submitted, 6509 } 6510 6511 if wt.BlockNumber == 0 { 6512 dcr.pendingTxsMtx.Lock() 6513 dcr.pendingTxs[*txHash] = ewt 6514 dcr.pendingTxsMtx.Unlock() 6515 } 6516 6517 err := txHistoryDB.StoreTx(ewt) 6518 if err != nil { 6519 dcr.log.Errorf("failed to store tx in tx history db: %v", err) 6520 } 6521 6522 skipNote := len(skipNotes) > 0 && skipNotes[0] 6523 if submitted && !skipNote { 6524 dcr.emit.TransactionNote(wt, true) 6525 } 6526 } 6527 6528 func (dcr *ExchangeWallet) checkPeers(ctx context.Context) { 6529 ctx, cancel := context.WithTimeout(ctx, 2*time.Second) 6530 defer cancel() 6531 numPeers, err := dcr.wallet.PeerCount(ctx) 6532 if err != nil { // e.g. dcrd passthrough fail in non-SPV mode 6533 prevPeer := atomic.SwapUint32(&dcr.lastPeerCount, 0) 6534 if prevPeer != 0 { 6535 dcr.log.Errorf("Failed to get peer count: %v", err) 6536 dcr.peersChange(0, err) 6537 } 6538 return 6539 } 6540 prevPeer := atomic.SwapUint32(&dcr.lastPeerCount, numPeers) 6541 if prevPeer != numPeers { 6542 dcr.peersChange(numPeers, nil) 6543 } 6544 } 6545 6546 func (dcr *ExchangeWallet) monitorPeers(ctx context.Context) { 6547 ticker := time.NewTicker(peerCountTicker) 6548 defer ticker.Stop() 6549 for { 6550 dcr.checkPeers(ctx) 6551 6552 select { 6553 case <-ticker.C: 6554 case <-ctx.Done(): 6555 return 6556 } 6557 } 6558 } 6559 6560 func (dcr *ExchangeWallet) emitTipChange(height int64) { 6561 var data any 6562 sinfo, err := dcr.wallet.StakeInfo(dcr.ctx) 6563 if err != nil { 6564 dcr.log.Errorf("Error getting stake info for tip change notification data: %v", err) 6565 } else { 6566 data = &struct { 6567 TicketPrice uint64 `json:"ticketPrice"` 6568 VotingSubsidy uint64 `json:"votingSubsidy"` 6569 Stats asset.TicketStats `json:"stats"` 6570 }{ 6571 TicketPrice: uint64(sinfo.Sdiff), 6572 Stats: dcr.ticketStatsFromStakeInfo(sinfo), 6573 VotingSubsidy: dcr.voteSubsidy(height), 6574 } 6575 } 6576 6577 dcr.emit.TipChange(uint64(height), data) 6578 go dcr.runTicketBuyer() 6579 } 6580 6581 func (dcr *ExchangeWallet) emitBalance() { 6582 if bal, err := dcr.Balance(); err != nil { 6583 dcr.log.Errorf("Error getting balance after mempool tx notification: %v", err) 6584 } else { 6585 dcr.emit.BalanceChange(bal) 6586 } 6587 } 6588 6589 // monitorBlocks pings for new blocks and runs the tipChange callback function 6590 // when the block changes. New blocks are also scanned for potential contract 6591 // redeems. 6592 func (dcr *ExchangeWallet) monitorBlocks(ctx context.Context) { 6593 ticker := time.NewTicker(blockTicker) 6594 defer ticker.Stop() 6595 6596 var walletBlock <-chan *block 6597 if notifier, isNotifier := dcr.wallet.(tipNotifier); isNotifier { 6598 walletBlock = notifier.tipFeed() 6599 } 6600 6601 // A polledBlock is a block found during polling, but whose broadcast has 6602 // been queued in anticipation of a wallet notification. 6603 type polledBlock struct { 6604 *block 6605 queue *time.Timer 6606 } 6607 6608 // queuedBlock is the currently queued, polling-discovered block that will 6609 // be broadcast after a timeout if the wallet doesn't send the matching 6610 // notification. 6611 var queuedBlock *polledBlock 6612 6613 // checkTip captures queuedBlock and walletBlock. 6614 checkTip := func() { 6615 ctxInternal, cancel0 := context.WithTimeout(ctx, 4*time.Second) 6616 defer cancel0() 6617 6618 newTip, err := dcr.getBestBlock(ctxInternal) 6619 if err != nil { 6620 dcr.log.Errorf("failed to get best block: %v", err) 6621 return 6622 } 6623 6624 if dcr.cachedBestBlock().hash.IsEqual(newTip.hash) { 6625 return 6626 } 6627 6628 if walletBlock == nil { 6629 dcr.handleTipChange(ctx, newTip.hash, newTip.height) 6630 return 6631 } 6632 6633 // Queue it for reporting, but don't send it right away. Give the wallet 6634 // a chance to provide their block update. SPV wallet may need more time 6635 // after storing the block header to fetch and scan filters and issue 6636 // the FilteredBlockConnected report. 6637 if queuedBlock != nil { 6638 queuedBlock.queue.Stop() 6639 } 6640 blockAllowance := walletBlockAllowance 6641 ctxInternal, cancel1 := context.WithTimeout(ctx, 4*time.Second) 6642 defer cancel1() 6643 ss, err := dcr.wallet.SyncStatus(ctxInternal) 6644 if err != nil { 6645 dcr.log.Errorf("Error retrieving sync status before queuing polled block: %v", err) 6646 } else if !ss.Synced { 6647 blockAllowance *= 10 6648 } 6649 queuedBlock = &polledBlock{ 6650 block: newTip, 6651 queue: time.AfterFunc(blockAllowance, func() { 6652 if ss, _ := dcr.SyncStatus(); ss != nil && ss.Synced { 6653 dcr.log.Warnf("Reporting a block found in polling that the wallet apparently "+ 6654 "never reported: %s (%d). If you see this message repeatedly, it may indicate "+ 6655 "an issue with the wallet.", newTip.hash, newTip.height) 6656 } 6657 dcr.handleTipChange(ctx, newTip.hash, newTip.height) 6658 }), 6659 } 6660 } 6661 6662 for { 6663 select { 6664 case <-ticker.C: 6665 checkTip() 6666 6667 case walletTip := <-walletBlock: 6668 if walletTip == nil { 6669 // Mempool tx seen. 6670 dcr.emitBalance() 6671 6672 tip := dcr.cachedBestBlock() 6673 dcr.syncTxHistory(ctx, uint64(tip.height)) 6674 continue 6675 } 6676 if queuedBlock != nil && walletTip.height >= queuedBlock.height { 6677 if !queuedBlock.queue.Stop() && walletTip.hash == queuedBlock.hash { 6678 continue 6679 } 6680 queuedBlock = nil 6681 } 6682 dcr.handleTipChange(ctx, walletTip.hash, walletTip.height) 6683 case <-ctx.Done(): 6684 return 6685 } 6686 6687 // Ensure context cancellation takes priority before the next iteration. 6688 if ctx.Err() != nil { 6689 return 6690 } 6691 } 6692 } 6693 6694 func (dcr *ExchangeWallet) handleTipChange(ctx context.Context, newTipHash *chainhash.Hash, newTipHeight int64) { 6695 if dcr.ctx.Err() != nil { 6696 return 6697 } 6698 6699 // Lock to avoid concurrent handleTipChange execution for simplicity. 6700 dcr.handleTipMtx.Lock() 6701 defer dcr.handleTipMtx.Unlock() 6702 6703 prevTip := dcr.currentTip.Swap(&block{newTipHeight, newTipHash}).(*block) 6704 6705 dcr.log.Tracef("tip change: %d (%s) => %d (%s)", prevTip.height, prevTip.hash, newTipHeight, newTipHash) 6706 6707 dcr.emitTipChange(newTipHeight) 6708 6709 if dcr.cycleMixer != nil { 6710 dcr.cycleMixer() 6711 } 6712 6713 dcr.wg.Add(1) 6714 go func() { 6715 dcr.syncTxHistory(ctx, uint64(newTipHeight)) 6716 dcr.wg.Done() 6717 }() 6718 6719 // Search for contract redemption in new blocks if there 6720 // are contracts pending redemption. 6721 dcr.findRedemptionMtx.RLock() 6722 pendingContractsCount := len(dcr.findRedemptionQueue) 6723 contractOutpoints := make([]outPoint, 0, pendingContractsCount) 6724 for contractOutpoint := range dcr.findRedemptionQueue { 6725 contractOutpoints = append(contractOutpoints, contractOutpoint) 6726 } 6727 dcr.findRedemptionMtx.RUnlock() 6728 if pendingContractsCount == 0 { 6729 return 6730 } 6731 6732 startHeight := prevTip.height + 1 6733 6734 // Redemption search would be compromised if the starting point cannot 6735 // be determined, as searching just the new tip might result in blocks 6736 // being omitted from the search operation. If that happens, cancel all 6737 // find redemption requests in queue. 6738 notifyFatalFindRedemptionError := func(s string, a ...any) { 6739 dcr.fatalFindRedemptionsError(fmt.Errorf("tipChange handler - "+s, a...), contractOutpoints) 6740 } 6741 6742 // Check if the previous tip is still part of the mainchain (prevTip confs >= 0). 6743 // Redemption search would typically resume from prevTip.height + 1 unless the 6744 // previous tip was re-orged out of the mainchain, in which case redemption 6745 // search will resume from the mainchain ancestor of the previous tip. 6746 prevTipHeader, isMainchain, _, err := dcr.blockHeader(ctx, prevTip.hash) 6747 if err != nil { 6748 // Redemption search cannot continue reliably without knowing if there 6749 // was a reorg, cancel all find redemption requests in queue. 6750 notifyFatalFindRedemptionError("blockHeader error for prev tip hash %s: %w", 6751 prevTip.hash, err) 6752 return 6753 } 6754 if !isMainchain { 6755 // The previous tip is no longer part of the mainchain. Crawl blocks 6756 // backwards until finding a mainchain block. Start with the block 6757 // that is the immediate ancestor to the previous tip. 6758 ancestorBlockHash, ancestorHeight, err := dcr.mainchainAncestor(ctx, &prevTipHeader.PrevBlock) 6759 if err != nil { 6760 notifyFatalFindRedemptionError("find mainchain ancestor for prev block: %s: %w", prevTipHeader.PrevBlock, err) 6761 return 6762 } 6763 6764 dcr.log.Debugf("reorg detected during tip change from height %d (%s) to %d (%s)", 6765 ancestorHeight, ancestorBlockHash, newTipHeight, newTipHash) 6766 6767 startHeight = ancestorHeight // have to recheck orphaned blocks again 6768 } 6769 6770 // Run the redemption search from the startHeight determined above up 6771 // till the current tip height. 6772 dcr.findRedemptionsInBlockRange(startHeight, newTipHeight, contractOutpoints) 6773 } 6774 6775 func (dcr *ExchangeWallet) getBestBlock(ctx context.Context) (*block, error) { 6776 hash, height, err := dcr.wallet.GetBestBlock(ctx) 6777 if err != nil { 6778 return nil, err 6779 } 6780 return &block{hash: hash, height: height}, nil 6781 } 6782 6783 // mainchainAncestor crawls blocks backwards starting at the provided hash 6784 // until finding a mainchain block. Returns the first mainchain block found. 6785 func (dcr *ExchangeWallet) mainchainAncestor(ctx context.Context, blockHash *chainhash.Hash) (*chainhash.Hash, int64, error) { 6786 checkHash := blockHash 6787 for { 6788 checkBlock, isMainchain, _, err := dcr.blockHeader(ctx, checkHash) 6789 if err != nil { 6790 return nil, 0, fmt.Errorf("getblockheader error for block %s: %w", checkHash, err) 6791 } 6792 if isMainchain { 6793 // This is a mainchain block, return the hash and height. 6794 return checkHash, int64(checkBlock.Height), nil 6795 } 6796 if checkBlock.Height == 0 { 6797 // Crawled back to genesis block without finding a mainchain ancestor 6798 // for the previous tip. Should never happen! 6799 return nil, 0, fmt.Errorf("no mainchain ancestor found for block %s", blockHash) 6800 } 6801 checkHash = &checkBlock.PrevBlock 6802 } 6803 } 6804 6805 // blockHeader returns the *BlockHeader for the specified block hash, and bools 6806 // indicating if the block is mainchain, and approved by stakeholders. 6807 // validMainchain will always be false if mainchain is false; mainchain can be 6808 // true for an invalidated block. 6809 func (dcr *ExchangeWallet) blockHeader(ctx context.Context, blockHash *chainhash.Hash) (blockHeader *BlockHeader, mainchain, validMainchain bool, err error) { 6810 blockHeader, err = dcr.wallet.GetBlockHeader(ctx, blockHash) 6811 if err != nil { 6812 return nil, false, false, fmt.Errorf("GetBlockHeader error for block %s: %w", blockHash, err) 6813 } 6814 if blockHeader.Confirmations < 0 { // not mainchain, really just == -1, but catch all unexpected 6815 dcr.log.Warnf("Block %v is a SIDE CHAIN block at height %d!", blockHash, blockHeader.Height) 6816 return blockHeader, false, false, nil 6817 } 6818 6819 // It's mainchain. Now check if there is a validating block. 6820 if blockHeader.NextHash == nil { // we're at the tip 6821 return blockHeader, true, true, nil 6822 } 6823 6824 nextHeader, err := dcr.wallet.GetBlockHeader(ctx, blockHeader.NextHash) 6825 if err != nil { 6826 return nil, false, false, fmt.Errorf("error fetching validating block: %w", err) 6827 } 6828 6829 validMainchain = nextHeader.VoteBits&1 != 0 6830 if !validMainchain { 6831 dcr.log.Warnf("Block %v found in mainchain, but stakeholder DISAPPROVED!", blockHash) 6832 } 6833 return blockHeader, true, validMainchain, nil 6834 } 6835 6836 func (dcr *ExchangeWallet) cachedBestBlock() *block { 6837 return dcr.currentTip.Load().(*block) 6838 } 6839 6840 // wireBytes dumps the serialized transaction bytes. 6841 func (dcr *ExchangeWallet) wireBytes(tx *wire.MsgTx) []byte { 6842 s, err := tx.Bytes() 6843 // wireBytes is just used for logging, and a serialization error is 6844 // extremely unlikely, so just log the error and return the nil bytes. 6845 if err != nil { 6846 dcr.log.Errorf("error serializing transaction: %v", err) 6847 } 6848 return s 6849 } 6850 6851 // Convert the DCR value to atoms. 6852 func toAtoms(v float64) uint64 { 6853 return uint64(math.Round(v * conventionalConversionFactor)) 6854 } 6855 6856 // toCoinID converts the tx hash and vout to a coin ID, as a []byte. 6857 func toCoinID(txHash *chainhash.Hash, vout uint32) []byte { 6858 coinID := make([]byte, chainhash.HashSize+4) 6859 copy(coinID[:chainhash.HashSize], txHash[:]) 6860 binary.BigEndian.PutUint32(coinID[chainhash.HashSize:], vout) 6861 return coinID 6862 } 6863 6864 // decodeCoinID decodes the coin ID into a tx hash and a vout. 6865 func decodeCoinID(coinID dex.Bytes) (*chainhash.Hash, uint32, error) { 6866 if len(coinID) != 36 { 6867 return nil, 0, fmt.Errorf("coin ID wrong length. expected 36, got %d", len(coinID)) 6868 } 6869 var txHash chainhash.Hash 6870 copy(txHash[:], coinID[:32]) 6871 return &txHash, binary.BigEndian.Uint32(coinID[32:]), nil 6872 } 6873 6874 // reduceMsgTx computes the total input and output amounts, the resulting 6875 // absolute fee and fee rate, and the serialized transaction size. 6876 func reduceMsgTx(tx *wire.MsgTx) (in, out, fees, rate, size uint64) { 6877 for _, txIn := range tx.TxIn { 6878 in += uint64(txIn.ValueIn) 6879 } 6880 for _, txOut := range tx.TxOut { 6881 out += uint64(txOut.Value) 6882 } 6883 fees = in - out 6884 size = uint64(tx.SerializeSize()) 6885 rate = fees / size 6886 return 6887 } 6888 6889 // toDCR returns a float representation in conventional units for the given 6890 // atoms. 6891 func toDCR[V uint64 | int64](v V) float64 { 6892 return dcrutil.Amount(v).ToCoin() 6893 } 6894 6895 // calcBumpedRate calculated a bump on the baseRate. If bump is nil, the 6896 // baseRate is returned directly. If *bump is out of range, an error is 6897 // returned. 6898 func calcBumpedRate(baseRate uint64, bump *float64) (uint64, error) { 6899 if bump == nil { 6900 return baseRate, nil 6901 } 6902 userBump := *bump 6903 if userBump > 2.0 { 6904 return baseRate, fmt.Errorf("fee bump %f is higher than the 2.0 limit", userBump) 6905 } 6906 if userBump < 1.0 { 6907 return baseRate, fmt.Errorf("fee bump %f is lower than 1", userBump) 6908 } 6909 return uint64(math.Round(float64(baseRate) * userBump)), nil 6910 } 6911 6912 func float64PtrStr(v *float64) string { 6913 if v == nil { 6914 return "nil" 6915 } 6916 return strconv.FormatFloat(*v, 'f', 8, 64) 6917 } 6918 6919 // WalletTransaction returns a transaction that either the wallet has made or 6920 // one in which the wallet has received funds. The txID should be either a 6921 // coin ID or a transaction hash in hexadecimal form. 6922 func (dcr *ExchangeWallet) WalletTransaction(ctx context.Context, txID string) (*asset.WalletTransaction, error) { 6923 coinID, err := hex.DecodeString(txID) 6924 if err == nil { 6925 txHash, _, err := decodeCoinID(coinID) 6926 if err == nil { 6927 txID = txHash.String() 6928 } 6929 } 6930 6931 txHistoryDB := dcr.txDB() 6932 if txHistoryDB == nil { 6933 return nil, fmt.Errorf("tx database not initialized") 6934 } 6935 6936 tx, err := txHistoryDB.GetTx(txID) 6937 if err != nil && !errors.Is(err, asset.CoinNotFoundError) { 6938 return nil, err 6939 } 6940 if tx != nil && tx.Confirmed { 6941 return tx, nil 6942 } 6943 6944 txHash, err := chainhash.NewHashFromStr(txID) 6945 if err != nil { 6946 return nil, fmt.Errorf("error decoding txid %s: %w", txID, err) 6947 } 6948 gtr, err := dcr.wallet.GetTransaction(ctx, txHash) 6949 if err != nil { 6950 return nil, fmt.Errorf("error getting transaction %s: %w", txID, err) 6951 } 6952 if len(gtr.Details) == 0 { 6953 return nil, fmt.Errorf("no details found for transaction %s", txID) 6954 } 6955 6956 var blockHeight, blockTime uint64 6957 if gtr.BlockHash != "" { 6958 blockHash, err := chainhash.NewHashFromStr(gtr.BlockHash) 6959 if err != nil { 6960 return nil, fmt.Errorf("error decoding block hash %s: %w", gtr.BlockHash, err) 6961 } 6962 blockHeader, err := dcr.wallet.GetBlockHeader(ctx, blockHash) 6963 if err != nil { 6964 return nil, fmt.Errorf("error getting block header for block %s: %w", blockHash, err) 6965 } 6966 blockHeight = uint64(blockHeader.Height) 6967 blockTime = uint64(blockHeader.Timestamp.Unix()) 6968 } 6969 6970 updated := tx == nil 6971 if tx == nil { 6972 blockIndex := int64(blockHeight) 6973 regularTx := walletjson.LTTTRegular 6974 tx, err = dcr.idUnknownTx(ctx, &ListTransactionsResult{ 6975 TxID: txID, 6976 BlockIndex: &blockIndex, 6977 BlockTime: int64(blockTime), 6978 Send: gtr.Details[0].Category == "send", 6979 TxType: ®ularTx, 6980 }) 6981 if err != nil { 6982 return nil, fmt.Errorf("xerror identifying transaction: %v", err) 6983 } 6984 } 6985 6986 if tx.BlockNumber != blockHeight || tx.Timestamp != blockTime { 6987 tx.BlockNumber = blockHeight 6988 tx.Timestamp = blockTime 6989 updated = true 6990 } 6991 6992 if updated { 6993 dcr.addTxToHistory(tx, txHash, true) 6994 } 6995 6996 // If the wallet knows about the transaction, it will be part of the 6997 // available balance, so we always return Confirmed = true. 6998 tx.Confirmed = true 6999 return tx, nil 7000 } 7001 7002 // TxHistory returns all the transactions the wallet has made. If refID is nil, 7003 // then transactions starting from the most recent are returned (past is ignored). 7004 // If past is true, the transactions prior to the refID are returned, otherwise 7005 // the transactions after the refID are returned. n is the number of 7006 // transactions to return. If n is <= 0, all the transactions will be returned. 7007 func (dcr *ExchangeWallet) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { 7008 txHistoryDB := dcr.txDB() 7009 if txHistoryDB == nil { 7010 return nil, fmt.Errorf("tx database not initialized") 7011 } 7012 7013 return txHistoryDB.GetTxs(n, refID, past) 7014 } 7015 7016 // ConfirmRedemption returns how many confirmations a redemption has. Normally 7017 // this is very straightforward. However there are two situations that have come 7018 // up that this also handles. One is when the wallet can not find the redemption 7019 // transaction. This is most likely because the fee was set too low and the tx 7020 // was removed from the mempool. In the case where it is not found, this will 7021 // send a new tx using the provided fee suggestion. The second situation 7022 // this watches for is a transaction that we can find but has been sitting in 7023 // the mempool for a long time. This has been observed with the wallet in SPV 7024 // mode and the transaction inputs having been spent by another transaction. The 7025 // wallet will not pick up on this so we could tell it to abandon the original 7026 // transaction and, again, send a new one using the provided feeSuggestion, but 7027 // only warning for now. This method should not be run for the same redemption 7028 // concurrently as it need to watch a new redeem transaction before finishing. 7029 func (dcr *ExchangeWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, feeSuggestion uint64) (*asset.ConfirmRedemptionStatus, error) { 7030 txHash, _, err := decodeCoinID(coinID) 7031 if err != nil { 7032 return nil, err 7033 } 7034 7035 var secretHash [32]byte 7036 copy(secretHash[:], redemption.Spends.SecretHash) 7037 dcr.mempoolRedeemsMtx.RLock() 7038 mRedeem, have := dcr.mempoolRedeems[secretHash] 7039 dcr.mempoolRedeemsMtx.RUnlock() 7040 7041 var deleteMempoolRedeem bool 7042 defer func() { 7043 if deleteMempoolRedeem { 7044 dcr.mempoolRedeemsMtx.Lock() 7045 delete(dcr.mempoolRedeems, secretHash) 7046 dcr.mempoolRedeemsMtx.Unlock() 7047 } 7048 }() 7049 7050 tx, err := dcr.wallet.GetTransaction(dcr.ctx, txHash) 7051 if err != nil && !errors.Is(err, asset.CoinNotFoundError) { 7052 return nil, fmt.Errorf("problem searching for redemption transaction %s: %w", txHash, err) 7053 } 7054 if err == nil { 7055 if have && mRedeem.txHash == *txHash { 7056 if tx.Confirmations == 0 && time.Now().After(mRedeem.firstSeen.Add(maxRedeemMempoolAge)) { 7057 // Transaction has been sitting in the mempool 7058 // for a long time now. 7059 // 7060 // TODO: Consider abandoning. 7061 redeemAge := time.Since(mRedeem.firstSeen) 7062 dcr.log.Warnf("Redemption transaction %v has been in the mempool for %v which is too long.", txHash, redeemAge) 7063 } 7064 } else { 7065 if have { 7066 // This should not happen. Core has told us to 7067 // watch a new redeem with a different transaction 7068 // hash for a trade we were already watching. 7069 return nil, fmt.Errorf("tx were were watching %s for redeem with secret hash %x being "+ 7070 "replaced by tx %s. core should not be replacing the transaction. maybe ConfirmRedemption "+ 7071 "is being run concurrently for the same redeem", mRedeem.txHash, secretHash, *txHash) 7072 } 7073 // Will hit this if bisonw was restarted with an actively 7074 // redeeming swap. 7075 dcr.mempoolRedeemsMtx.Lock() 7076 dcr.mempoolRedeems[secretHash] = &mempoolRedeem{txHash: *txHash, firstSeen: time.Now()} 7077 dcr.mempoolRedeemsMtx.Unlock() 7078 } 7079 if tx.Confirmations >= requiredRedeemConfirms { 7080 deleteMempoolRedeem = true 7081 } 7082 return &asset.ConfirmRedemptionStatus{ 7083 Confs: uint64(tx.Confirmations), 7084 Req: requiredRedeemConfirms, 7085 CoinID: coinID, 7086 }, nil 7087 } 7088 7089 // Redemption transaction is missing from the point of view of our wallet! 7090 // Unlikely, but possible it was redeemed by another transaction. We 7091 // assume a contract past its locktime cannot make it here, so it must 7092 // not be refunded. Check if the contract is still an unspent output. 7093 7094 swapHash, vout, err := decodeCoinID(redemption.Spends.Coin.ID()) 7095 if err != nil { 7096 return nil, err 7097 } 7098 7099 _, _, spentStatus, err := dcr.lookupTxOutput(dcr.ctx, swapHash, vout) 7100 if err != nil { 7101 return nil, fmt.Errorf("error finding unspent contract: %w", err) 7102 } 7103 7104 switch spentStatus { 7105 case -1, 1: 7106 // First find the block containing the output itself. 7107 scriptAddr, err := stdaddr.NewAddressScriptHashV0(redemption.Spends.Contract, dcr.chainParams) 7108 if err != nil { 7109 return nil, fmt.Errorf("error encoding contract address: %w", err) 7110 } 7111 _, pkScript := scriptAddr.PaymentScript() 7112 outFound, block, err := dcr.externalTxOutput(dcr.ctx, newOutPoint(swapHash, vout), 7113 pkScript, time.Now().Add(-60*24*time.Hour)) // search up to 60 days ago 7114 if err != nil { 7115 return nil, err // possibly the contract is still in mempool 7116 } 7117 spent, err := dcr.isOutputSpent(dcr.ctx, outFound) 7118 if err != nil { 7119 return nil, fmt.Errorf("error checking if contract %v:%d is spent: %w", *swapHash, vout, err) 7120 } 7121 if !spent { 7122 break 7123 } 7124 vin := -1 7125 spendTx := outFound.spenderTx 7126 for i := range spendTx.TxIn { 7127 sigScript := spendTx.TxIn[i].SignatureScript 7128 sigScriptLen := len(sigScript) 7129 if sigScriptLen < dexdcr.SwapContractSize { 7130 continue 7131 } 7132 // The spent contract is at the end of the signature 7133 // script. Lop off the front half. 7134 script := sigScript[sigScriptLen-dexdcr.SwapContractSize:] 7135 _, _, _, sh, err := dexdcr.ExtractSwapDetails(script, dcr.chainParams) 7136 if err != nil { 7137 // This is not our script, but not necessarily 7138 // a problem. 7139 dcr.log.Tracef("Error encountered searching for the input that spends %v, "+ 7140 "extracting swap details from vin %d of %d. Probably not a problem: %v.", 7141 spendTx.TxHash(), i, len(spendTx.TxIn), err) 7142 continue 7143 } 7144 if bytes.Equal(sh[:], secretHash[:]) { 7145 vin = i 7146 break 7147 } 7148 } 7149 if vin >= 0 { 7150 _, height, err := dcr.wallet.GetBestBlock(dcr.ctx) 7151 if err != nil { 7152 return nil, err 7153 } 7154 confs := uint64(height - block.height) 7155 hash := spendTx.TxHash() 7156 if confs < requiredRedeemConfirms { 7157 dcr.mempoolRedeemsMtx.Lock() 7158 dcr.mempoolRedeems[secretHash] = &mempoolRedeem{txHash: hash, firstSeen: time.Now()} 7159 dcr.mempoolRedeemsMtx.Unlock() 7160 } 7161 return &asset.ConfirmRedemptionStatus{ 7162 Confs: confs, 7163 Req: requiredRedeemConfirms, 7164 CoinID: toCoinID(&hash, uint32(vin)), 7165 }, nil 7166 } 7167 dcr.log.Warnf("Contract coin %v spent by someone but not sure who.", redemption.Spends.Coin.ID()) 7168 // Incorrect, but we will be in a loop of erroring if we don't 7169 // return something. We were unable to find the spender for some 7170 // reason. 7171 7172 // May be still in the map if abandonTx failed. 7173 deleteMempoolRedeem = true 7174 7175 return &asset.ConfirmRedemptionStatus{ 7176 Confs: requiredRedeemConfirms, 7177 Req: requiredRedeemConfirms, 7178 CoinID: coinID, 7179 }, nil 7180 } 7181 7182 // The contract has not yet been redeemed, but it seems the redeeming 7183 // tx has disappeared. Assume the fee was too low at the time and it 7184 // was eventually purged from the mempool. Attempt to redeem again with 7185 // a currently reasonable fee. 7186 7187 form := &asset.RedeemForm{ 7188 Redemptions: []*asset.Redemption{redemption}, 7189 FeeSuggestion: feeSuggestion, 7190 } 7191 _, coin, _, err := dcr.Redeem(form) 7192 if err != nil { 7193 return nil, fmt.Errorf("unable to re-redeem %s: %w", redemption.Spends.Coin.ID(), err) 7194 } 7195 7196 coinID = coin.ID() 7197 newRedeemHash, _, err := decodeCoinID(coinID) 7198 if err != nil { 7199 return nil, err 7200 } 7201 7202 dcr.mempoolRedeemsMtx.Lock() 7203 dcr.mempoolRedeems[secretHash] = &mempoolRedeem{txHash: *newRedeemHash, firstSeen: time.Now()} 7204 dcr.mempoolRedeemsMtx.Unlock() 7205 7206 return &asset.ConfirmRedemptionStatus{ 7207 Confs: 0, 7208 Req: requiredRedeemConfirms, 7209 CoinID: coinID, 7210 }, nil 7211 } 7212 7213 var _ asset.GeocodeRedeemer = (*ExchangeWallet)(nil) 7214 7215 // RedeemGeocode redeems funds from a geocode game tx to this wallet. 7216 func (dcr *ExchangeWallet) RedeemGeocode(code []byte, msg string) (dex.Bytes, uint64, error) { 7217 msgLen := len([]byte(msg)) 7218 if msgLen > stdscript.MaxDataCarrierSizeV0 { 7219 return nil, 0, fmt.Errorf("message is too long. must be %d > %d", msgLen, stdscript.MaxDataCarrierSizeV0) 7220 } 7221 7222 k, err := hdkeychain.NewMaster(code, dcr.chainParams) 7223 if err != nil { 7224 return nil, 0, fmt.Errorf("error generating key from bond: %w", err) 7225 } 7226 gameKey, err := k.SerializedPrivKey() 7227 if err != nil { 7228 return nil, 0, fmt.Errorf("error serializing private key: %w", err) 7229 } 7230 7231 gamePub := k.SerializedPubKey() 7232 gameAddr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(dcrutil.Hash160(gamePub), dcr.chainParams) 7233 if err != nil { 7234 return nil, 0, fmt.Errorf("error generating address: %w", err) 7235 } 7236 7237 gameTxs, err := getDcrdataTxs(dcr.ctx, gameAddr.String(), dcr.network) 7238 if err != nil { 7239 return nil, 0, fmt.Errorf("error getting tx from dcrdata: %w", err) 7240 } 7241 7242 _, gameScript := gameAddr.PaymentScript() 7243 7244 feeRate, err := dcr.feeRate(2) 7245 if err != nil { 7246 return nil, 0, fmt.Errorf("error getting tx fee rate: %w", err) 7247 } 7248 7249 redeemTx := wire.NewMsgTx() 7250 var redeemable int64 7251 for _, gameTx := range gameTxs { 7252 txHash := gameTx.TxHash() 7253 for vout, txOut := range gameTx.TxOut { 7254 if bytes.Equal(txOut.PkScript, gameScript) { 7255 redeemable += txOut.Value 7256 prevOut := wire.NewOutPoint(&txHash, uint32(vout), wire.TxTreeRegular) 7257 redeemTx.AddTxIn(wire.NewTxIn(prevOut, txOut.Value, gameScript)) 7258 } 7259 } 7260 } 7261 7262 if len(redeemTx.TxIn) == 0 { 7263 return nil, 0, fmt.Errorf("no spendable game outputs found in %d txs for address %s", len(gameTxs), gameAddr) 7264 } 7265 7266 var txSize uint64 = dexdcr.MsgTxOverhead + uint64(len(redeemTx.TxIn))*dexdcr.P2PKHInputSize + dexdcr.P2PKHOutputSize 7267 if msgLen > 0 { 7268 txSize += dexdcr.TxOutOverhead + 1 /* opreturn */ + uint64(wire.VarIntSerializeSize(uint64(msgLen))) + uint64(msgLen) 7269 } 7270 fees := feeRate * txSize 7271 7272 if uint64(redeemable) < fees { 7273 return nil, 0, fmt.Errorf("estimated fees %d are less than the redeemable value %d", fees, redeemable) 7274 } 7275 win := uint64(redeemable) - fees 7276 if dexdcr.IsDustVal(dexdcr.P2PKHOutputSize, win, feeRate) { 7277 return nil, 0, fmt.Errorf("received value is dust after fees: %d - %d = %d", redeemable, fees, win) 7278 } 7279 7280 redeemAddr, err := dcr.wallet.ExternalAddress(dcr.ctx, dcr.depositAccount()) 7281 if err != nil { 7282 return nil, 0, fmt.Errorf("error getting redeem address: %w", err) 7283 } 7284 _, redeemScript := redeemAddr.PaymentScript() 7285 7286 redeemTx.AddTxOut(wire.NewTxOut(int64(win), redeemScript)) 7287 if msgLen > 0 { 7288 msgScript, err := txscript.NewScriptBuilder().AddOp(txscript.OP_RETURN).AddData([]byte(msg)).Script() 7289 if err != nil { 7290 return nil, 0, fmt.Errorf("error building message script: %w", err) 7291 } 7292 redeemTx.AddTxOut(wire.NewTxOut(0, msgScript)) 7293 } 7294 7295 for vin, txIn := range redeemTx.TxIn { 7296 redeemInSig, err := sign.RawTxInSignature(redeemTx, vin, gameScript, txscript.SigHashAll, 7297 gameKey, dcrec.STEcdsaSecp256k1) 7298 if err != nil { 7299 return nil, 0, fmt.Errorf("error creating signature for input script: %w", err) 7300 } 7301 txIn.SignatureScript, err = txscript.NewScriptBuilder().AddData(redeemInSig).AddData(gamePub).Script() 7302 if err != nil { 7303 return nil, 0, fmt.Errorf("error building p2pkh sig script: %w", err) 7304 } 7305 } 7306 7307 redeemHash, err := dcr.broadcastTx(redeemTx) 7308 if err != nil { 7309 return nil, 0, fmt.Errorf("error broadcasting tx: %w", err) 7310 } 7311 7312 return toCoinID(redeemHash, 0), win, nil 7313 } 7314 7315 func getDcrdataTxs(ctx context.Context, addr string, net dex.Network) (txs []*wire.MsgTx, _ error) { 7316 apiRoot := "https://dcrdata.decred.org/api/" 7317 switch net { 7318 case dex.Testnet: 7319 apiRoot = "https://testnet.dcrdata.org/api/" 7320 case dex.Simnet: 7321 apiRoot = "http://127.0.0.1:17779/api/" 7322 } 7323 7324 var resp struct { 7325 Txs []struct { 7326 TxID string `json:"txid"` 7327 } `json:"address_transactions"` 7328 } 7329 if err := dexnet.Get(ctx, apiRoot+"address/"+addr, &resp); err != nil { 7330 return nil, fmt.Errorf("error getting address info for address %q: %w", addr, err) 7331 } 7332 for _, tx := range resp.Txs { 7333 txID := tx.TxID 7334 7335 // tx/hex response is a hex string but is not JSON encoded. 7336 r, err := http.DefaultClient.Get(apiRoot + "tx/hex/" + txID) 7337 if err != nil { 7338 return nil, fmt.Errorf("error getting transaction %q: %w", txID, err) 7339 } 7340 defer r.Body.Close() 7341 b, err := io.ReadAll(r.Body) 7342 if err != nil { 7343 return nil, fmt.Errorf("error reading response body: %w", err) 7344 } 7345 r.Body.Close() 7346 hexTx := string(b) 7347 txB, err := hex.DecodeString(hexTx) 7348 if err != nil { 7349 return nil, fmt.Errorf("error decoding hex for tx %q: %w", txID, err) 7350 } 7351 7352 tx, err := msgTxFromBytes(txB) 7353 if err != nil { 7354 return nil, fmt.Errorf("error deserializing tx %x: %w", txID, err) 7355 } 7356 txs = append(txs, tx) 7357 } 7358 7359 return 7360 }