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