decred.org/dcrdex@v1.0.5/server/asset/btc/btc.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 btc 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/sha256" 10 "encoding/binary" 11 "encoding/hex" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "math" 16 "net/http" 17 "os" 18 "strings" 19 "sync" 20 "time" 21 22 "decred.org/dcrdex/dex" 23 "decred.org/dcrdex/dex/calc" 24 "decred.org/dcrdex/dex/config" 25 "decred.org/dcrdex/dex/dexnet" 26 dexbtc "decred.org/dcrdex/dex/networks/btc" 27 dexzec "decred.org/dcrdex/dex/networks/zec" 28 "decred.org/dcrdex/dex/txfee" 29 "decred.org/dcrdex/server/account" 30 "decred.org/dcrdex/server/asset" 31 srvdex "decred.org/dcrdex/server/dex" 32 "github.com/btcsuite/btcd/blockchain" 33 "github.com/btcsuite/btcd/btcjson" 34 "github.com/btcsuite/btcd/btcutil" 35 "github.com/btcsuite/btcd/chaincfg" 36 "github.com/btcsuite/btcd/chaincfg/chainhash" 37 "github.com/btcsuite/btcd/wire" 38 "github.com/decred/dcrd/dcrjson/v4" // for dcrjson.RPCError returns from rpcclient 39 "github.com/decred/dcrd/rpcclient/v8" 40 ) 41 42 const defaultNoCompetitionRate = 10 43 44 type v1Config struct { 45 ConfigPath string `json:"configPath"` 46 DisableAPIFees bool `json:"disableApiFees"` 47 TatumKey string `json:"tatumKey"` 48 BlockdaemonKey string `json:"blockdaemonKey"` 49 } 50 51 // Driver implements asset.Driver. 52 type Driver struct{} 53 54 // Setup creates the BTC backend. Start the backend with its Run method. 55 func (d *Driver) Setup(cfg *asset.BackendConfig) (asset.Backend, error) { 56 return NewBackend(cfg) 57 } 58 59 // DecodeCoinID creates a human-readable representation of a coin ID for 60 // Bitcoin. 61 func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { 62 txid, vout, err := decodeCoinID(coinID) 63 if err != nil { 64 return "", err 65 } 66 return fmt.Sprintf("%v:%d", txid, vout), err 67 } 68 69 // Version returns the Backend implementation's version number. 70 func (d *Driver) Version() uint32 { 71 return version 72 } 73 74 // UnitInfo returns the dex.UnitInfo for the asset. 75 func (d *Driver) UnitInfo() dex.UnitInfo { 76 return dexbtc.UnitInfo 77 } 78 79 // MinBondSize calculates the minimum bond size for a given fee rate that avoids 80 // dust outputs on the bond and refund txs, assuming the maxFeeRate doesn't 81 // change. 82 func (d *Driver) MinBondSize(maxFeeRate uint64) uint64 { 83 return dexbtc.MinBondSize(maxFeeRate, true) 84 } 85 86 // MinLotSize calculates the minimum bond size for a given fee rate that avoids 87 // dust outputs on the swap and refund txs, assuming the maxFeeRate doesn't 88 // change. 89 func (d *Driver) MinLotSize(maxFeeRate uint64) uint64 { 90 return dexbtc.MinLotSize(maxFeeRate, true) 91 } 92 93 // Name is the asset's name. 94 func (d *Driver) Name() string { 95 return "Bitcoin" 96 } 97 98 func init() { 99 asset.Register(BipID, &Driver{}) 100 101 if blockPollIntervalStr != "" { 102 blockPollInterval, _ = time.ParseDuration(blockPollIntervalStr) 103 if blockPollInterval < time.Second { 104 panic(fmt.Sprintf("invalid value for blockPollIntervalStr: %q", blockPollIntervalStr)) 105 } 106 } 107 } 108 109 var ( 110 zeroHash chainhash.Hash 111 // The blockPollInterval is the delay between calls to GetBestBlockHash to 112 // check for new blocks. Modify at compile time via blockPollIntervalStr: 113 // go build -ldflags "-X 'decred.org/dcrdex/server/asset/btc.blockPollIntervalStr=4s'" 114 blockPollInterval time.Duration 115 blockPollIntervalStr string 116 conventionalConversionFactor = float64(dexbtc.UnitInfo.Conventional.ConversionFactor) 117 defaultMaxFeeBlocks = 3 118 ) 119 120 const ( 121 version = 0 122 BipID = 0 123 assetName = "btc" 124 immatureTransactionError = dex.ErrorKind("immature output") 125 BondVersion = 0 126 ) 127 128 func netParams(network dex.Network) (*chaincfg.Params, error) { 129 var params *chaincfg.Params 130 switch network { 131 case dex.Simnet: 132 params = &chaincfg.RegressionNetParams 133 case dex.Testnet: 134 params = &chaincfg.TestNet3Params 135 case dex.Mainnet: 136 params = &chaincfg.MainNetParams 137 default: 138 return nil, fmt.Errorf("unknown network ID: %d", uint8(network)) 139 } 140 return params, nil 141 } 142 143 // Backend is a dex backend for Bitcoin or a Bitcoin clone. It has methods for 144 // fetching UTXO information and subscribing to block updates. It maintains a 145 // cache of block data for quick lookups. Backend implements asset.Backend, so 146 // provides exported methods for DEX-related blockchain info. 147 type Backend struct { 148 rpcCfg *dexbtc.RPCConfig 149 cfg *BackendCloneConfig 150 // The asset name (e.g. btc), primarily for logging purposes. 151 name string 152 // segwit should be set to true for blockchains that support segregated 153 // witness. 154 segwit bool 155 initTxSizeBase, initTxSize uint64 156 // node is used throughout for RPC calls. For testing, it can be set to a stub. 157 node *RPCClient 158 // The block cache stores just enough info about the blocks to shortcut future 159 // calls to GetBlockVerbose. 160 blockCache *blockCache 161 // The backend provides block notification channels through it BlockChannel 162 // method. signalMtx locks the blockChans array. 163 signalMtx sync.RWMutex 164 blockChans map[chan *asset.BlockUpdate]struct{} 165 chainParams *chaincfg.Params 166 // A logger will be provided by the dex for this backend. All logging should 167 // use the provided logger. 168 log dex.Logger 169 decodeAddr dexbtc.AddressDecoder 170 // booleanGetBlockRPC corresponds to BackendCloneConfig.BooleanGetBlockRPC 171 // field and is used by RPCClient, which is constructed on Connect. 172 booleanGetBlockRPC bool 173 blockDeserializer func([]byte) (*wire.MsgBlock, error) // may be nil 174 txDeserializer func([]byte) (*wire.MsgTx, error) // must not be nil 175 txHasher func(*wire.MsgTx) *chainhash.Hash 176 numericGetRawRPC bool 177 // fee estimation configuration 178 feeConfs int64 179 noCompetitionRate uint64 180 181 // The feeCache prevents repeated calculations of the median fee rate 182 // between block changes when estimate(smart)fee is unprimed. 183 feeCache struct { 184 sync.Mutex 185 fee uint64 186 hash chainhash.Hash 187 } 188 189 feeRateCache struct { 190 sync.RWMutex 191 feeRate uint64 192 stamp time.Time 193 } 194 } 195 196 // Check that Backend satisfies the Backend interface. 197 var _ asset.Backend = (*Backend)(nil) 198 var _ srvdex.Bonder = (*Backend)(nil) 199 200 // NewBackend is the exported constructor by which the DEX will import the 201 // backend. The configPath can be an empty string, in which case the standard 202 // system location of the bitcoind config file is assumed. 203 func NewBackend(cfg *asset.BackendConfig) (asset.Backend, error) { 204 params, err := netParams(cfg.Net) 205 if err != nil { 206 return nil, err 207 } 208 209 configPath := cfg.ConfigPath 210 if configPath == "" { 211 configPath = dexbtc.SystemConfigPath("bitcoin") 212 } 213 214 feeSources := make([]*txfee.SourceConfig, len(freeFeeSources), len(freeFeeSources)+1) 215 copy(feeSources, freeFeeSources) 216 b, err := os.ReadFile(configPath) 217 if err != nil { 218 return nil, fmt.Errorf("error reading config file: %w", err) 219 } 220 var cfgV1 v1Config 221 if err = json.Unmarshal(b, &cfgV1); err == nil { 222 if cfgV1.ConfigPath == "" { 223 return nil, errors.New("no config path defined in v1 config file") 224 } 225 configPath = cfgV1.ConfigPath 226 227 if cfgV1.TatumKey != "" { 228 feeSources = append(feeSources, tatumFeeFetcher(cfgV1.TatumKey)) 229 } 230 if cfgV1.BlockdaemonKey != "" { 231 feeSources = append(feeSources, blockDaemonFeeFetcher(cfgV1.BlockdaemonKey)) 232 } 233 } 234 var feeFetcher *txfee.FeeFetcher 235 if !cfgV1.DisableAPIFees { 236 feeFetcher = txfee.NewFeeFetcher(feeSources, cfg.Logger) 237 } 238 return NewBTCClone(&BackendCloneConfig{ 239 Name: assetName, 240 Segwit: true, 241 ConfigPath: configPath, 242 Logger: cfg.Logger, 243 Net: cfg.Net, 244 ChainParams: params, 245 Ports: dexbtc.RPCPorts, 246 RelayAddr: cfg.RelayAddr, 247 FeeFetcher: feeFetcher, 248 }) 249 } 250 251 func newBTC(cloneCfg *BackendCloneConfig, rpcCfg *dexbtc.RPCConfig) *Backend { 252 addrDecoder := btcutil.DecodeAddress 253 if cloneCfg.AddressDecoder != nil { 254 addrDecoder = cloneCfg.AddressDecoder 255 } 256 257 noCompetitionRate := cloneCfg.NoCompetitionFeeRate 258 if noCompetitionRate == 0 { 259 noCompetitionRate = defaultNoCompetitionRate 260 } 261 262 feeConfs := cloneCfg.FeeConfs 263 if feeConfs == 0 { 264 feeConfs = 1 265 } 266 267 txDeserializer := cloneCfg.TxDeserializer 268 if txDeserializer == nil { 269 txDeserializer = msgTxFromBytes 270 } 271 272 txHasher := cloneCfg.TxHasher 273 if txHasher == nil { 274 txHasher = hashTx 275 } 276 277 initTxSize, initTxSizeBase := uint64(dexbtc.InitTxSize), uint64(dexbtc.InitTxSizeBase) 278 switch { 279 case cloneCfg.Segwit: 280 initTxSize, initTxSizeBase = dexbtc.InitTxSizeSegwit, dexbtc.InitTxSizeBaseSegwit 281 case cloneCfg.Name == "zcl": 282 initTxSize, initTxSizeBase = dexzec.InitTxSize, dexzec.InitTxSizeBase 283 } 284 285 return &Backend{ 286 rpcCfg: rpcCfg, 287 cfg: cloneCfg, 288 name: cloneCfg.Name, 289 blockCache: newBlockCache(), 290 blockChans: make(map[chan *asset.BlockUpdate]struct{}), 291 chainParams: cloneCfg.ChainParams, 292 log: cloneCfg.Logger, 293 segwit: cloneCfg.Segwit, 294 initTxSizeBase: initTxSizeBase, 295 initTxSize: initTxSize, 296 decodeAddr: addrDecoder, 297 noCompetitionRate: noCompetitionRate, 298 feeConfs: feeConfs, 299 booleanGetBlockRPC: cloneCfg.BooleanGetBlockRPC, 300 blockDeserializer: cloneCfg.BlockDeserializer, 301 txDeserializer: txDeserializer, 302 txHasher: txHasher, 303 numericGetRawRPC: cloneCfg.NumericGetRawRPC, 304 } 305 } 306 307 // BackendCloneConfig captures the arguments necessary to configure a BTC clone 308 // backend. 309 type BackendCloneConfig struct { 310 Name string 311 Segwit bool 312 ConfigPath string 313 AddressDecoder dexbtc.AddressDecoder 314 Logger dex.Logger 315 Net dex.Network 316 ChainParams *chaincfg.Params 317 Ports dexbtc.NetPorts 318 // ManualFeeScan specifies that median block fees should be calculated by 319 // scanning transactions since the getblockstats rpc is not available. 320 // Median block fees are used to estimate fee rates when the cache is not 321 // primed. 322 ManualMedianFee bool 323 // NoCompetitionFeeRate specifies a fee rate to use if estimatesmartfee 324 // or estimatefee aren't ready and the median fee is finding relatively 325 // empty blocks. 326 NoCompetitionFeeRate uint64 327 // DumbFeeEstimates is for asset's whose RPC is estimatefee instead of 328 // estimatesmartfee. 329 DumbFeeEstimates bool 330 // Argsless fee estimates are for assets who don't take an argument for 331 // number of blocks to estimatefee. 332 ArglessFeeEstimates bool 333 // FeeConfs specifies the target number of confirmations to use for 334 // estimate(smart)fee. If not set, default value is 1, 335 FeeConfs int64 336 // MaxFeeBlocks is the maximum number of blocks that can be evaluated for 337 // median fee calculations. If > 100 txs are not seen in the last 338 // MaxFeeBlocks, then the NoCompetitionRate will be returned as the median 339 // fee. 340 MaxFeeBlocks int 341 // BooleanGetBlockRPC will pass true instead of 2 as the getblock argument. 342 BooleanGetBlockRPC bool 343 // BlockDeserializer can be used in place of (*wire.MsgBlock).Deserialize. 344 BlockDeserializer func(blk []byte) (*wire.MsgBlock, error) 345 // TxDeserializer is an optional function used to deserialize a transaction. 346 // TxDeserializer is only used if ManualMedianFee is true. 347 TxDeserializer func([]byte) (*wire.MsgTx, error) 348 // TxHasher is a function that generates a tx hash from a MsgTx. 349 TxHasher func(*wire.MsgTx) *chainhash.Hash 350 // BlockFeeTransactions is a function to fetch a set of FeeTx and a previous 351 // block hash for a specific block. 352 BlockFeeTransactions BlockFeeTransactions 353 // NumericGetRawRPC uses a numeric boolean indicator for the 354 // getrawtransaction RPC. 355 NumericGetRawRPC bool 356 // ShieldedIO is a function to read a transaction and calculate the shielded 357 // input and output amounts. This is a temporary measure until zcashd 358 // encodes valueBalanceOrchard in their getrawtransaction RPC results. 359 ShieldedIO func(tx *VerboseTxExtended) (in, out uint64, err error) 360 // RelayAddr is an address for a NodeRelay. 361 RelayAddr string 362 FeeFetcher *txfee.FeeFetcher 363 } 364 365 // NewBTCClone creates a BTC backend for a set of network parameters and default 366 // network ports. A BTC clone can use this method, possibly in conjunction with 367 // ReadCloneParams, to create a Backend for other assets with minimal coding. 368 // See ReadCloneParams and CompatibilityCheck for more info. 369 func NewBTCClone(cloneCfg *BackendCloneConfig) (*Backend, error) { 370 // Read the configuration parameters 371 rpcConfig := new(dexbtc.RPCConfig) 372 err := config.ParseInto(cloneCfg.ConfigPath, rpcConfig) 373 if err != nil { 374 return nil, err 375 } 376 if cloneCfg.RelayAddr != "" { 377 rpcConfig.RPCBind = cloneCfg.RelayAddr 378 } 379 380 err = dexbtc.CheckRPCConfig(rpcConfig, cloneCfg.Name, cloneCfg.Net, cloneCfg.Ports) 381 if err != nil { 382 return nil, err 383 } 384 return newBTC(cloneCfg, rpcConfig), nil 385 } 386 387 func (btc *Backend) shutdown() { 388 if btc.node != nil { 389 btc.node.requester.Shutdown() 390 btc.node.requester.WaitForShutdown() 391 } 392 } 393 394 // Connect connects to the node RPC server. A dex.Connector. 395 func (btc *Backend) Connect(ctx context.Context) (*sync.WaitGroup, error) { 396 client, err := rpcclient.New(&rpcclient.ConnConfig{ 397 HTTPPostMode: true, 398 DisableTLS: !btc.rpcCfg.IsPublicProvider, 399 Host: btc.rpcCfg.RPCBind, 400 User: btc.rpcCfg.RPCUser, 401 Pass: btc.rpcCfg.RPCPass, 402 }, nil) 403 if err != nil { 404 return nil, fmt.Errorf("error creating %q RPC client: %w", btc.name, err) 405 } 406 407 maxFeeBlocks := btc.cfg.MaxFeeBlocks 408 if maxFeeBlocks == 0 { 409 maxFeeBlocks = defaultMaxFeeBlocks 410 } 411 412 blockFeeTransactions := btc.cfg.BlockFeeTransactions 413 if blockFeeTransactions == nil { 414 blockFeeTransactions = btcBlockFeeTransactions 415 } 416 417 btc.node = &RPCClient{ 418 ctx: ctx, 419 requester: client, 420 booleanGetBlockRPC: btc.booleanGetBlockRPC, 421 maxFeeBlocks: maxFeeBlocks, 422 arglessFeeEstimates: btc.cfg.ArglessFeeEstimates, 423 blockDeserializer: btc.blockDeserializer, 424 numericGetRawRPC: btc.numericGetRawRPC, 425 deserializeTx: btc.txDeserializer, 426 blockFeeTransactions: blockFeeTransactions, 427 } 428 429 // Prime the cache 430 bestHash, err := btc.node.GetBestBlockHash() 431 if err != nil { 432 btc.shutdown() 433 return nil, fmt.Errorf("error getting best block from rpc: %w", err) 434 } 435 if bestHash != nil { 436 if _, err = btc.getBtcBlock(bestHash); err != nil { 437 btc.shutdown() 438 return nil, fmt.Errorf("error priming the cache: %w", err) 439 } 440 } 441 442 // Assume public RPC providers have txindex, or maybe want to check an old 443 // transaction or something, but the getindexinfo method may not be 444 // available for public providers. 445 txindex := btc.rpcCfg.IsPublicProvider 446 if !txindex { 447 txindex, err = btc.node.checkTxIndex() 448 if err != nil { 449 btc.log.Warnf(`Please ensure txindex is enabled in the node config and you might need to re-index if txindex was not previously enabled for %s`, btc.name) 450 btc.shutdown() 451 return nil, fmt.Errorf("error checking txindex for %s: %w", btc.name, err) 452 } 453 } 454 if !txindex { 455 btc.shutdown() 456 return nil, fmt.Errorf("%s transaction index is not enabled. Please enable txindex in the node config and you might need to re-index when you enable txindex", btc.name) 457 } 458 459 var wg sync.WaitGroup 460 461 if fetcher := btc.cfg.FeeFetcher; fetcher != nil { 462 cm := dex.NewConnectionMaster(btc.cfg.FeeFetcher) 463 if err := cm.ConnectOnce(ctx); err != nil { 464 btc.shutdown() 465 return nil, fmt.Errorf("error starting fee fetcher: %w", err) 466 } 467 wg.Add(1) 468 go func() { 469 defer wg.Done() 470 defer cm.Disconnect() 471 for { 472 select { 473 case r := <-fetcher.Next(): 474 btc.log.Tracef("New fee reported: %d", r) 475 btc.feeRateCache.Lock() 476 btc.feeRateCache.stamp = time.Now() 477 btc.feeRateCache.feeRate = r 478 btc.feeRateCache.Unlock() 479 case <-ctx.Done(): 480 return 481 } 482 } 483 }() 484 } 485 486 if _, err = btc.estimateFee(ctx); err != nil { 487 btc.log.Warnf("Backend started without fee estimation available: %v", err) 488 } 489 490 wg.Add(1) 491 go func() { 492 defer wg.Done() 493 btc.run(ctx) 494 }() 495 return &wg, nil 496 } 497 498 // Net returns the *chaincfg.Params. This is not part of the asset.Backend 499 // interface, and is exported as a convenience for embedding types. 500 func (btc *Backend) Net() *chaincfg.Params { 501 return btc.chainParams 502 } 503 504 // Contract is part of the asset.Backend interface. An asset.Contract is an 505 // output that has been validated as a swap contract for the passed redeem 506 // script. A spendable output is one that can be spent in the next block. Every 507 // output from a non-coinbase transaction is spendable immediately. Coinbase 508 // outputs are only spendable after CoinbaseMaturity confirmations. Pubkey 509 // scripts can be P2PKH or P2SH. Multi-sig P2SH redeem scripts are supported. 510 func (btc *Backend) Contract(coinID []byte, redeemScript []byte) (*asset.Contract, error) { 511 txHash, vout, err := decodeCoinID(coinID) 512 if err != nil { 513 return nil, fmt.Errorf("error decoding coin ID %x: %w", coinID, err) 514 } 515 output, err := btc.output(txHash, vout, redeemScript) 516 if err != nil { 517 return nil, err 518 } 519 // Verify contract and set refundAddress and swapAddress. 520 return btc.auditContract(output) 521 } 522 523 // ValidateSecret checks that the secret satisfies the contract. 524 func (btc *Backend) ValidateSecret(secret, contract []byte) bool { 525 _, _, _, secretHash, err := dexbtc.ExtractSwapDetails(contract, btc.segwit, btc.chainParams) 526 if err != nil { 527 btc.log.Errorf("ValidateSecret->ExtractSwapDetails error: %v\n", err) 528 return false 529 } 530 h := sha256.Sum256(secret) 531 return bytes.Equal(h[:], secretHash) 532 } 533 534 // Synced is true if the blockchain is ready for action. 535 func (btc *Backend) Synced() (bool, error) { 536 chainInfo, err := btc.node.GetBlockChainInfo() 537 if err != nil { 538 return false, fmt.Errorf("GetBlockChainInfo error: %w", err) 539 } 540 return !chainInfo.InitialBlockDownload && chainInfo.Headers-chainInfo.Blocks <= 1, nil 541 } 542 543 // Redemption is an input that redeems a swap contract. 544 func (btc *Backend) Redemption(redemptionID, contractID, _ []byte) (asset.Coin, error) { 545 txHash, vin, err := decodeCoinID(redemptionID) 546 if err != nil { 547 return nil, fmt.Errorf("error decoding redemption coin ID %x: %w", txHash, err) 548 } 549 input, err := btc.input(txHash, vin) 550 if err != nil { 551 return nil, err 552 } 553 spends, err := input.spendsCoin(contractID) 554 if err != nil { 555 return nil, err 556 } 557 if !spends { 558 return nil, fmt.Errorf("%x does not spend %x", redemptionID, contractID) 559 } 560 return input, nil 561 } 562 563 // FundingCoin is an unspent output. 564 func (btc *Backend) FundingCoin(_ context.Context, coinID []byte, redeemScript []byte) (asset.FundingCoin, error) { 565 txHash, vout, err := decodeCoinID(coinID) 566 if err != nil { 567 return nil, fmt.Errorf("error decoding coin ID %x: %w", coinID, err) 568 } 569 570 utxo, err := btc.utxo(txHash, vout, redeemScript) 571 if err != nil { 572 return nil, err 573 } 574 575 if utxo.nonStandardScript { 576 return nil, fmt.Errorf("non-standard script") 577 } 578 return utxo, nil 579 } 580 581 func (btc *Backend) ValidateOrderFunding(swapVal, valSum, _, inputsSize, maxSwaps uint64, nfo *dex.Asset) bool { 582 reqVal := calc.RequiredOrderFunds(swapVal, inputsSize, maxSwaps, btc.initTxSizeBase, btc.initTxSize, nfo.MaxFeeRate) 583 return valSum >= reqVal 584 } 585 586 // ValidateCoinID attempts to decode the coinID. 587 func (btc *Backend) ValidateCoinID(coinID []byte) (string, error) { 588 txid, vout, err := decodeCoinID(coinID) 589 if err != nil { 590 return "", err 591 } 592 return fmt.Sprintf("%v:%d", txid, vout), err 593 } 594 595 // ValidateContract ensures that the swap contract is constructed properly, and 596 // contains valid sender and receiver addresses. 597 func (btc *Backend) ValidateContract(contract []byte) error { 598 _, _, _, _, err := dexbtc.ExtractSwapDetails(contract, btc.segwit, btc.chainParams) 599 return err 600 } 601 602 // VerifyUnspentCoin attempts to verify a coin ID by decoding the coin ID and 603 // retrieving the corresponding UTXO. If the coin is not found or no longer 604 // unspent, an asset.CoinNotFoundError is returned. 605 func (btc *Backend) VerifyUnspentCoin(_ context.Context, coinID []byte) error { 606 txHash, vout, err := decodeCoinID(coinID) 607 if err != nil { 608 return fmt.Errorf("error decoding coin ID %x: %w", coinID, err) 609 } 610 txOut, err := btc.node.GetTxOut(txHash, vout, true) 611 if err != nil { 612 return fmt.Errorf("GetTxOut (%s:%d): %w", txHash.String(), vout, err) 613 } 614 if txOut == nil { 615 return asset.CoinNotFoundError 616 } 617 return nil 618 } 619 620 // ParseBondTx performs basic validation of a serialized time-locked fidelity 621 // bond transaction given the bond's P2SH or P2WSH redeem script. 622 // 623 // The transaction must have at least two outputs: out 0 pays to a P2SH address 624 // (the bond), and out 1 is a nulldata output that commits to an account ID. 625 // There may also be a change output. 626 // 627 // Returned: The bond's coin ID (i.e. encoded UTXO) of the bond output. The bond 628 // output's amount and P2SH/P2WSH address. The lockTime and pubkey hash data pushes 629 // from the script. The account ID from the second output is also returned. 630 // 631 // Properly formed transactions: 632 // 633 // 1. The bond output (vout 0) must be a P2SH/P2WSH output. 634 // 2. The bond's redeem script must be of the form: 635 // <lockTime[4]> OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 <pubkeyhash[20]> OP_EQUALVERIFY OP_CHECKSIG 636 // 3. The null data output (vout 1) must have a 58-byte data push (ver | account ID | lockTime | pubkeyHash). 637 // 4. The transaction must have a zero locktime and expiry. 638 // 5. All inputs must have the max sequence num set (finalized). 639 // 6. The transaction must pass the checks in the 640 // blockchain.CheckTransactionSanity function. 641 func ParseBondTx(ver uint16, msgTx *wire.MsgTx, chainParams *chaincfg.Params, segwit bool) (amt int64, bondAddr string, 642 bondPubKeyHash []byte, lockTime int64, acct account.AccountID, err error) { 643 if ver != BondVersion { 644 err = errors.New("only version 0 bonds supported") 645 return 646 } 647 648 if msgTx.LockTime != 0 { 649 err = errors.New("transaction locktime not zero") 650 return 651 } 652 if err = blockchain.CheckTransactionSanity(btcutil.NewTx(msgTx)); err != nil { 653 return 654 } 655 656 if len(msgTx.TxOut) < 2 { 657 err = fmt.Errorf("expected at least 2 outputs, found %d", len(msgTx.TxOut)) 658 return 659 } 660 661 for _, txIn := range msgTx.TxIn { 662 if txIn.Sequence != wire.MaxTxInSequenceNum { 663 err = errors.New("input has non-max sequence number") 664 return 665 } 666 } 667 668 // Fidelity bond (output 0) 669 bondOut := msgTx.TxOut[0] 670 scriptHash := dexbtc.ExtractScriptHash(bondOut.PkScript) 671 if scriptHash == nil { 672 err = fmt.Errorf("bad bond pkScript") 673 return 674 } 675 switch len(scriptHash) { 676 case 32: 677 if !segwit { 678 err = fmt.Errorf("%s backend does not support segwit bonds", chainParams.Name) 679 return 680 } 681 case 20: 682 if segwit { 683 err = fmt.Errorf("%s backend requires segwit bonds", chainParams.Name) 684 return 685 } 686 default: 687 err = fmt.Errorf("unexpected script hash length %d", len(scriptHash)) 688 return 689 } 690 691 acctCommitOut := msgTx.TxOut[1] 692 acct, lock, pkh, err := dexbtc.ExtractBondCommitDataV0(0, acctCommitOut.PkScript) 693 if err != nil { 694 err = fmt.Errorf("invalid bond commitment output: %w", err) 695 return 696 } 697 698 // Reconstruct and check the bond redeem script. 699 bondScript, err := dexbtc.MakeBondScript(ver, lock, pkh[:]) 700 if err != nil { 701 err = fmt.Errorf("failed to build bond output redeem script: %w", err) 702 return 703 } 704 705 // Check that the script hash extracted from output 0 is what is expected 706 // based on the information in the account commitment. 707 // P2WSH uses sha256, while P2SH uses ripemd160(sha256). 708 var expectedScriptHash []byte 709 if len(scriptHash) == 32 { 710 hash := sha256.Sum256(bondScript) 711 expectedScriptHash = hash[:] 712 } else { 713 expectedScriptHash = btcutil.Hash160(bondScript) 714 } 715 if !bytes.Equal(expectedScriptHash, scriptHash) { 716 err = fmt.Errorf("script hash check failed for output 0") 717 return 718 } 719 720 _, addrs, _, err := dexbtc.ExtractScriptData(bondOut.PkScript, chainParams) 721 if err != nil { 722 err = fmt.Errorf("error extracting addresses from bond output: %w", err) 723 return 724 } 725 amt = bondOut.Value 726 bondAddr = addrs[0] // don't convert address, must match type we specified 727 lockTime = int64(lock) 728 bondPubKeyHash = pkh[:] 729 730 return 731 } 732 733 // BondVer returns the latest supported bond version. 734 func (dcr *Backend) BondVer() uint16 { 735 return BondVersion 736 } 737 738 // ParseBondTx makes the package-level ParseBondTx pure function accessible via 739 // a Backend instance. This performs basic validation of a serialized 740 // time-locked fidelity bond transaction given the bond's P2SH redeem script. 741 func (btc *Backend) ParseBondTx(ver uint16, rawTx []byte) (bondCoinID []byte, amt int64, bondAddr string, 742 bondPubKeyHash []byte, lockTime int64, acct account.AccountID, err error) { 743 var msgTx *wire.MsgTx 744 msgTx, err = btc.txDeserializer(rawTx) 745 if err != nil { 746 return 747 } 748 bondCoinID = toCoinID(btc.txHasher(msgTx), 0) 749 amt, bondAddr, bondPubKeyHash, lockTime, acct, err = ParseBondTx(ver, msgTx, btc.chainParams, btc.segwit) 750 return 751 } 752 753 // BondCoin locates a bond transaction output, validates the entire transaction, 754 // and returns the amount, encoded lockTime and account ID, and the 755 // confirmations of the transaction. It is a CoinNotFoundError if the 756 // transaction output is spent. 757 func (btc *Backend) BondCoin(ctx context.Context, ver uint16, coinID []byte) (amt, lockTime, confs int64, acct account.AccountID, err error) { 758 txHash, vout, errCoin := decodeCoinID(coinID) 759 if errCoin != nil { 760 err = fmt.Errorf("error decoding coin ID %x: %w", coinID, errCoin) 761 return 762 } 763 764 verboseTx, err := btc.node.GetRawTransactionVerbose(txHash) 765 if err != nil { 766 if isTxNotFoundErr(err) { 767 err = asset.CoinNotFoundError 768 } 769 return 770 } 771 772 if int(vout) > len(verboseTx.Vout)-1 { 773 err = fmt.Errorf("invalid output index for tx with %d outputs", len(verboseTx.Vout)) 774 return 775 } 776 777 confs = int64(verboseTx.Confirmations) 778 779 var msgTx *wire.MsgTx 780 msgTx, err = btc.txDeserializer(verboseTx.Raw) 781 if err != nil { 782 return 783 } 784 785 txOut, err := btc.node.GetTxOut(txHash, vout, true) // check regular tree first 786 if err != nil { 787 if isTxNotFoundErr(err) { // should be txOut==nil, but checking anyway 788 err = asset.CoinNotFoundError 789 return 790 } 791 return 792 } 793 if txOut == nil { // spent == invalid bond 794 err = asset.CoinNotFoundError 795 return 796 } 797 798 amt, _, _, lockTime, acct, err = ParseBondTx(ver, msgTx, btc.chainParams, btc.segwit) 799 return 800 } 801 802 // txOutData is transaction output data, including recipient addresses, value, 803 // script type, and number of required signatures. 804 type txOutData struct { 805 value uint64 806 addresses []string 807 sigsRequired int 808 scriptType dexbtc.BTCScriptType 809 } 810 811 // outputSummary gets transaction output data, including recipient addresses, 812 // value, script type, and number of required signatures, plus the current 813 // confirmations of a transaction output. If the output does not exist, an error 814 // will be returned. Non-standard scripts are not an error. 815 func (btc *Backend) outputSummary(txHash *chainhash.Hash, vout uint32) (txOut *txOutData, confs int64, err error) { 816 var verboseTx *VerboseTxExtended 817 verboseTx, err = btc.node.GetRawTransactionVerbose(txHash) 818 if err != nil { 819 if isTxNotFoundErr(err) { 820 err = asset.CoinNotFoundError 821 } 822 return 823 } 824 825 if int(vout) > len(verboseTx.Vout)-1 { 826 err = asset.CoinNotFoundError // should be something fatal? 827 return 828 } 829 830 out := verboseTx.Vout[vout] 831 832 script, err := hex.DecodeString(out.ScriptPubKey.Hex) 833 if err != nil { 834 return nil, -1, dex.UnsupportedScriptError 835 } 836 scriptType, addrs, numRequired, err := dexbtc.ExtractScriptData(script, btc.chainParams) 837 if err != nil { 838 return nil, -1, dex.UnsupportedScriptError 839 } 840 841 txOut = &txOutData{ 842 value: toSat(out.Value), 843 addresses: addrs, // out.ScriptPubKey.Addresses 844 sigsRequired: numRequired, // out.ScriptPubKey.ReqSigs 845 scriptType: scriptType, // integer representation of the string in out.ScriptPubKey.Type 846 } 847 848 confs = int64(verboseTx.Confirmations) 849 return 850 } 851 852 // BlockChannel creates and returns a new channel on which to receive block 853 // updates. If the returned channel is ever blocking, there will be no error 854 // logged from the btc package. Part of the asset.Backend interface. 855 func (btc *Backend) BlockChannel(size int) <-chan *asset.BlockUpdate { 856 c := make(chan *asset.BlockUpdate, size) 857 btc.signalMtx.Lock() 858 defer btc.signalMtx.Unlock() 859 btc.blockChans[c] = struct{}{} 860 return c 861 } 862 863 // FeeRate returns the current optimal fee rate in sat / byte. 864 func (btc *Backend) FeeRate(ctx context.Context) (uint64, error) { 865 return btc.estimateFee(ctx) 866 } 867 868 // Info provides some general information about the backend. 869 func (*Backend) Info() *asset.BackendInfo { 870 return &asset.BackendInfo{} 871 } 872 873 // ValidateFeeRate checks that the transaction fees used to initiate the 874 // contract are sufficient. 875 func (btc *Backend) ValidateFeeRate(c asset.Coin, reqFeeRate uint64) bool { 876 return c.FeeRate() >= reqFeeRate 877 } 878 879 // CheckSwapAddress checks that the given address is parseable, and suitable as 880 // a redeem address in a swap contract script. 881 func (btc *Backend) CheckSwapAddress(addr string) bool { 882 btcAddr, err := btc.decodeAddr(addr, btc.chainParams) 883 if err != nil { 884 btc.log.Errorf("CheckSwapAddress for %s failed: %v", addr, err) 885 return false 886 } 887 if btc.segwit { 888 if _, ok := btcAddr.(*btcutil.AddressWitnessPubKeyHash); !ok { 889 btc.log.Errorf("CheckSwapAddress for %s failed: not a witness-pubkey-hash address (%T)", 890 btcAddr.String(), btcAddr) 891 return false 892 } 893 } else { 894 if _, ok := btcAddr.(*btcutil.AddressPubKeyHash); !ok { 895 btc.log.Errorf("CheckSwapAddress for %s failed: not a pubkey-hash address (%T)", 896 btcAddr.String(), btcAddr) 897 return false 898 } 899 } 900 return true 901 } 902 903 // TxData is the raw transaction bytes. SPV clients rebroadcast the transaction 904 // bytes to get around not having a mempool to check. 905 func (btc *Backend) TxData(coinID []byte) ([]byte, error) { 906 txHash, _, err := decodeCoinID(coinID) 907 if err != nil { 908 return nil, err 909 } 910 txB, err := btc.node.GetRawTransaction(txHash) 911 if err != nil { 912 if isTxNotFoundErr(err) { 913 return nil, asset.CoinNotFoundError 914 } 915 return nil, fmt.Errorf("GetRawTransaction for txid %s: %w", txHash, err) 916 } 917 return txB, nil 918 } 919 920 // blockInfo returns block information for the verbose transaction data. The 921 // current tip hash is also returned as a convenience. 922 func (btc *Backend) blockInfo(verboseTx *VerboseTxExtended) (blockHeight uint32, blockHash chainhash.Hash, tipHash *chainhash.Hash, err error) { 923 h := btc.blockCache.tipHash() 924 if h != zeroHash { 925 tipHash = &h 926 } 927 if verboseTx.Confirmations > 0 { 928 var blk *cachedBlock 929 blk, err = btc.getBlockInfo(verboseTx.BlockHash) 930 if err != nil { 931 return 932 } 933 blockHeight = blk.height 934 blockHash = blk.hash 935 } 936 return 937 } 938 939 // Get the UTXO data and perform some checks for script support. 940 func (btc *Backend) utxo(txHash *chainhash.Hash, vout uint32, redeemScript []byte) (*UTXO, error) { 941 txOut, verboseTx, pkScript, err := btc.getTxOutInfo(txHash, vout) 942 if err != nil { 943 return nil, err 944 } 945 946 inputNfo, err := dexbtc.InputInfo(pkScript, redeemScript, btc.chainParams) 947 if err != nil { 948 return nil, err 949 } 950 scriptType := inputNfo.ScriptType 951 952 // If it's a pay-to-script-hash, extract the script hash and check it against 953 // the hash of the user-supplied redeem script. 954 if scriptType.IsP2SH() || scriptType.IsP2WSH() { 955 scriptHash := dexbtc.ExtractScriptHash(pkScript) 956 if scriptType.IsSegwit() { 957 shash := sha256.Sum256(redeemScript) 958 if !bytes.Equal(shash[:], scriptHash) { 959 return nil, fmt.Errorf("(utxo:segwit) script hash check failed for utxo %s,%d", txHash, vout) 960 } 961 } else { 962 if !bytes.Equal(btcutil.Hash160(redeemScript), scriptHash) { 963 return nil, fmt.Errorf("(utxo:non-segwit) script hash check failed for utxo %s,%d", txHash, vout) 964 } 965 } 966 } 967 968 // Get block information. 969 blockHeight, blockHash, lastLookup, err := btc.blockInfo(verboseTx) 970 if err != nil { 971 return nil, err 972 } 973 974 // Coinbase transactions must mature before spending. 975 var maturity int64 976 if txOut.Coinbase { 977 maturity = int64(btc.chainParams.CoinbaseMaturity) 978 } 979 if txOut.Confirmations < maturity { 980 return nil, immatureTransactionError 981 } 982 983 tx, err := btc.transaction(txHash, verboseTx) 984 if err != nil { 985 return nil, fmt.Errorf("error fetching verbose transaction data: %w", err) 986 } 987 988 out := &Output{ 989 TXIO: TXIO{ 990 btc: btc, 991 tx: tx, 992 height: blockHeight, 993 blockHash: blockHash, 994 maturity: int32(maturity), 995 lastLookup: lastLookup, 996 }, 997 vout: vout, 998 scriptType: scriptType, 999 nonStandardScript: inputNfo.NonStandardScript, 1000 pkScript: pkScript, 1001 redeemScript: redeemScript, 1002 numSigs: inputNfo.ScriptAddrs.NRequired, 1003 spendSize: inputNfo.VBytes(), 1004 value: toSat(txOut.Value), 1005 } 1006 return &UTXO{out}, nil 1007 } 1008 1009 // newTXIO creates a TXIO for a transaction, spent or unspent. 1010 func (btc *Backend) newTXIO(txHash *chainhash.Hash) (*TXIO, int64, error) { 1011 verboseTx, err := btc.node.GetRawTransactionVerbose(txHash) 1012 if err != nil { 1013 if isTxNotFoundErr(err) { 1014 return nil, 0, asset.CoinNotFoundError 1015 } 1016 return nil, 0, fmt.Errorf("GetRawTransactionVerbose for txid %s: %w", txHash, err) 1017 } 1018 tx, err := btc.transaction(txHash, verboseTx) 1019 if err != nil { 1020 return nil, 0, fmt.Errorf("error fetching verbose transaction data: %w", err) 1021 } 1022 blockHeight, blockHash, lastLookup, err := btc.blockInfo(verboseTx) 1023 if err != nil { 1024 return nil, 0, err 1025 } 1026 var maturity int32 1027 if tx.isCoinbase { 1028 maturity = int32(btc.chainParams.CoinbaseMaturity) 1029 } 1030 return &TXIO{ 1031 btc: btc, 1032 tx: tx, 1033 height: blockHeight, 1034 blockHash: blockHash, 1035 maturity: maturity, 1036 lastLookup: lastLookup, 1037 }, int64(verboseTx.Confirmations), nil 1038 } 1039 1040 // input gets the transaction input. 1041 func (btc *Backend) input(txHash *chainhash.Hash, vin uint32) (*Input, error) { 1042 txio, _, err := btc.newTXIO(txHash) 1043 if err != nil { 1044 return nil, err 1045 } 1046 if int(vin) >= len(txio.tx.ins) { 1047 return nil, fmt.Errorf("tx %v has %d outputs (no vin %d)", txHash, len(txio.tx.ins), vin) 1048 } 1049 return &Input{ 1050 TXIO: *txio, 1051 vin: vin, 1052 }, nil 1053 } 1054 1055 // output gets the transaction output. 1056 func (btc *Backend) output(txHash *chainhash.Hash, vout uint32, redeemScript []byte) (*Output, error) { 1057 txio, confs, err := btc.newTXIO(txHash) 1058 if err != nil { 1059 return nil, err 1060 } 1061 if int(vout) >= len(txio.tx.outs) { 1062 return nil, fmt.Errorf("tx %v has %d outputs (no vout %d)", txHash, len(txio.tx.outs), vout) 1063 } 1064 1065 txOut := txio.tx.outs[vout] 1066 pkScript := txOut.pkScript 1067 inputNfo, err := dexbtc.InputInfo(pkScript, redeemScript, btc.chainParams) 1068 if err != nil { 1069 return nil, err 1070 } 1071 scriptType := inputNfo.ScriptType 1072 1073 // If it's a pay-to-script-hash, extract the script hash and check it against 1074 // the hash of the user-supplied redeem script. 1075 if scriptType.IsP2SH() || scriptType.IsP2WSH() { 1076 scriptHash := dexbtc.ExtractScriptHash(pkScript) 1077 if scriptType.IsSegwit() { 1078 shash := sha256.Sum256(redeemScript) 1079 if !bytes.Equal(shash[:], scriptHash) { 1080 return nil, fmt.Errorf("(output:segwit) script hash check failed for utxo %s,%d", txHash, vout) 1081 } 1082 } else { 1083 if !bytes.Equal(btcutil.Hash160(redeemScript), scriptHash) { 1084 return nil, fmt.Errorf("(output:non-segwit) script hash check failed for utxo %s,%d", txHash, vout) 1085 } 1086 } 1087 } 1088 1089 scrAddrs := inputNfo.ScriptAddrs 1090 addresses := make([]string, scrAddrs.NumPK+scrAddrs.NumPKH) 1091 for i, addr := range append(scrAddrs.PkHashes, scrAddrs.PubKeys...) { 1092 addresses[i] = addr.String() // unconverted 1093 } 1094 1095 // Coinbase transactions must mature before spending. 1096 if confs < int64(txio.maturity) { 1097 return nil, immatureTransactionError 1098 } 1099 1100 return &Output{ 1101 TXIO: *txio, 1102 vout: vout, 1103 value: txOut.value, 1104 addresses: addresses, 1105 scriptType: scriptType, 1106 nonStandardScript: inputNfo.NonStandardScript, 1107 pkScript: pkScript, 1108 redeemScript: redeemScript, 1109 numSigs: scrAddrs.NRequired, 1110 // The total size associated with the wire.TxIn. 1111 spendSize: inputNfo.VBytes(), 1112 }, nil 1113 } 1114 1115 // Get the value of the previous outpoint. 1116 func (btc *Backend) prevOutputValue(txid string, vout int) (uint64, error) { 1117 txHash, err := chainhash.NewHashFromStr(txid) 1118 if err != nil { 1119 return 0, fmt.Errorf("error decoding tx hash %s: %w", txid, err) 1120 } 1121 verboseTx, err := btc.node.GetRawTransactionVerbose(txHash) 1122 if err != nil { 1123 return 0, err 1124 } 1125 if vout > len(verboseTx.Vout)-1 { 1126 return 0, fmt.Errorf("prevOutput: vout index out of range") 1127 } 1128 output := verboseTx.Vout[vout] 1129 return toSat(output.Value), nil 1130 } 1131 1132 // Get the Tx. Transaction info is not cached, so every call will result in a 1133 // GetRawTransactionVerbose RPC call. 1134 func (btc *Backend) transaction(txHash *chainhash.Hash, verboseTx *VerboseTxExtended) (*Tx, error) { 1135 // If it's not a mempool transaction, get and cache the block data. 1136 var blockHash *chainhash.Hash 1137 var lastLookup *chainhash.Hash 1138 var blockHeight int64 1139 if verboseTx.BlockHash == "" { 1140 tipHash := btc.blockCache.tipHash() 1141 if tipHash != zeroHash { 1142 lastLookup = &tipHash 1143 } 1144 } else { 1145 var err error 1146 blockHash, err = chainhash.NewHashFromStr(verboseTx.BlockHash) 1147 if err != nil { 1148 return nil, fmt.Errorf("error decoding block hash %s for tx %s: %w", verboseTx.BlockHash, txHash, err) 1149 } 1150 // Make sure the block info is cached. 1151 blk, err := btc.getBtcBlock(blockHash) 1152 if err != nil { 1153 return nil, fmt.Errorf("error caching the block data for transaction %s", txHash) 1154 } 1155 blockHeight = int64(blk.height) 1156 } 1157 1158 // Parse inputs and outputs, storing only what's needed. 1159 inputs := make([]txIn, 0, len(verboseTx.Vin)) 1160 1161 // sumIn, sumOut := verboseTx.ShieldedIO() 1162 var sumIn, sumOut uint64 1163 if btc.cfg.ShieldedIO != nil { 1164 var err error 1165 sumIn, sumOut, err = btc.cfg.ShieldedIO(verboseTx) 1166 if err != nil { 1167 return nil, fmt.Errorf("ShieldedIO error: %w", err) 1168 } 1169 } 1170 1171 var isCoinbase bool 1172 for vin, input := range verboseTx.Vin { 1173 isCoinbase = input.Coinbase != "" 1174 var valIn uint64 1175 if isCoinbase { 1176 valIn = toSat(verboseTx.Vout[0].Value) 1177 } else { 1178 var err error 1179 valIn, err = btc.prevOutputValue(input.Txid, int(input.Vout)) 1180 if err != nil { 1181 return nil, fmt.Errorf("error fetching previous output value for %s:%d: %w", txHash, vin, err) 1182 } 1183 } 1184 sumIn += valIn 1185 if input.Txid == "" { 1186 inputs = append(inputs, txIn{ 1187 vout: input.Vout, 1188 value: valIn, 1189 }) 1190 continue 1191 } 1192 hash, err := chainhash.NewHashFromStr(input.Txid) 1193 if err != nil { 1194 return nil, fmt.Errorf("error decoding previous tx hash %s for tx %s: %w", input.Txid, txHash, err) 1195 } 1196 inputs = append(inputs, txIn{ 1197 prevTx: *hash, 1198 vout: input.Vout, 1199 value: valIn, 1200 }) 1201 } 1202 1203 outputs := make([]txOut, 0, len(verboseTx.Vout)) 1204 for vout, output := range verboseTx.Vout { 1205 pkScript, err := hex.DecodeString(output.ScriptPubKey.Hex) 1206 if err != nil { 1207 return nil, fmt.Errorf("error decoding pubkey script from %s for transaction %d:%d: %w", 1208 output.ScriptPubKey.Hex, txHash, vout, err) 1209 } 1210 vOut := toSat(output.Value) 1211 sumOut += vOut 1212 outputs = append(outputs, txOut{ 1213 value: vOut, 1214 pkScript: pkScript, 1215 }) 1216 } 1217 1218 // TODO: Unneeded after https://github.com/ZclassicCommunity/zclassic/pull/82 1219 if verboseTx.Size == 0 { 1220 verboseTx.Size = int32(len(verboseTx.Raw)) 1221 } 1222 1223 var feeRate uint64 1224 if btc.segwit { 1225 if verboseTx.Vsize > 0 { 1226 feeRate = (sumIn - sumOut) / uint64(verboseTx.Vsize) 1227 } 1228 } else if verboseTx.Size > 0 && sumIn > sumOut { 1229 // For non-segwit transactions, Size = Vsize anyway, so use Size to 1230 // cover assets that won't set Vsize in their RPC response. 1231 feeRate = (sumIn - sumOut) / uint64(verboseTx.Size) 1232 } 1233 hash := blockHash 1234 if hash == nil { 1235 hash = &zeroHash 1236 } 1237 return &Tx{ 1238 btc: btc, 1239 blockHash: *hash, 1240 height: blockHeight, 1241 hash: *txHash, 1242 ins: inputs, 1243 outs: outputs, 1244 isCoinbase: isCoinbase, 1245 lastLookup: lastLookup, 1246 inputSum: sumIn, 1247 feeRate: feeRate, 1248 raw: verboseTx.Raw, 1249 }, nil 1250 } 1251 1252 // Get information for an unspent transaction output and it's transaction. 1253 func (btc *Backend) getTxOutInfo(txHash *chainhash.Hash, vout uint32) (*btcjson.GetTxOutResult, *VerboseTxExtended, []byte, error) { 1254 txOut, err := btc.node.GetTxOut(txHash, vout, true) 1255 if err != nil { 1256 if isTxNotFoundErr(err) { // should be txOut==nil, but checking anyway 1257 return nil, nil, nil, asset.CoinNotFoundError 1258 } 1259 return nil, nil, nil, fmt.Errorf("GetTxOut error for output %s:%d: %w", txHash, vout, err) 1260 } 1261 if txOut == nil { 1262 return nil, nil, nil, asset.CoinNotFoundError 1263 } 1264 pkScript, err := hex.DecodeString(txOut.ScriptPubKey.Hex) 1265 if err != nil { 1266 return nil, nil, nil, fmt.Errorf("failed to decode pubkey from '%s' for output %s:%d", txOut.ScriptPubKey.Hex, txHash, vout) 1267 } 1268 verboseTx, err := btc.node.GetRawTransactionVerbose(txHash) 1269 if err != nil { 1270 if isTxNotFoundErr(err) { 1271 return nil, nil, nil, asset.CoinNotFoundError // shouldn't happen if gettxout found it 1272 } 1273 return nil, nil, nil, fmt.Errorf("GetRawTransactionVerbose for txid %s: %w", txHash, err) 1274 } 1275 return txOut, verboseTx, pkScript, nil 1276 } 1277 1278 // Get the block information, checking the cache first. Same as 1279 // getBtcBlock, but takes a string argument. 1280 func (btc *Backend) getBlockInfo(blockid string) (*cachedBlock, error) { 1281 blockHash, err := chainhash.NewHashFromStr(blockid) 1282 if err != nil { 1283 return nil, fmt.Errorf("unable to decode block hash from %s", blockid) 1284 } 1285 return btc.getBtcBlock(blockHash) 1286 } 1287 1288 // Get the block information, checking the cache first. 1289 func (btc *Backend) getBtcBlock(blockHash *chainhash.Hash) (*cachedBlock, error) { 1290 cachedBlk, found := btc.blockCache.block(blockHash) 1291 if found { 1292 return cachedBlk, nil 1293 } 1294 blockVerbose, err := btc.node.GetBlockVerbose(blockHash) 1295 if err != nil { 1296 return nil, fmt.Errorf("error retrieving block %s: %w", blockHash, err) 1297 } 1298 return btc.blockCache.add(blockVerbose) 1299 } 1300 1301 // auditContract checks that output is a swap contract and extracts the 1302 // receiving address and contract value on success. 1303 func (btc *Backend) auditContract(contract *Output) (*asset.Contract, error) { 1304 tx := contract.tx 1305 if len(tx.outs) <= int(contract.vout) { 1306 return nil, fmt.Errorf("invalid index %d for transaction %s", contract.vout, tx.hash) 1307 } 1308 output := tx.outs[int(contract.vout)] 1309 1310 // If it's a pay-to-script-hash, extract the script hash and check it against 1311 // the hash of the user-supplied redeem script. 1312 scriptType := dexbtc.ParseScriptType(output.pkScript, contract.redeemScript) 1313 if scriptType == dexbtc.ScriptUnsupported { 1314 return nil, fmt.Errorf("specified output %s:%d is not P2SH", tx.hash, contract.vout) 1315 } 1316 var scriptHash, hashed []byte 1317 if scriptType.IsP2SH() || scriptType.IsP2WSH() { 1318 scriptHash = dexbtc.ExtractScriptHash(output.pkScript) 1319 if scriptType.IsSegwit() { 1320 if !btc.segwit { 1321 return nil, fmt.Errorf("segwit contract, but %s is not configured for segwit", btc.name) 1322 } 1323 shash := sha256.Sum256(contract.redeemScript) 1324 hashed = shash[:] 1325 } else { 1326 if btc.segwit { 1327 return nil, fmt.Errorf("non-segwit contract, but %s is configured for segwit", btc.name) 1328 } 1329 hashed = btcutil.Hash160(contract.redeemScript) 1330 } 1331 } 1332 if scriptHash == nil { 1333 return nil, fmt.Errorf("specified output %s:%d is not P2SH or P2WSH", tx.hash, contract.vout) 1334 } 1335 if !bytes.Equal(hashed, scriptHash) { 1336 return nil, fmt.Errorf("swap contract hash mismatch for %s:%d", tx.hash, contract.vout) 1337 } 1338 _, receiver, lockTime, secretHash, err := dexbtc.ExtractSwapDetails(contract.redeemScript, contract.btc.segwit, contract.btc.chainParams) 1339 if err != nil { 1340 return nil, fmt.Errorf("error parsing swap contract for %s:%d: %w", tx.hash, contract.vout, err) 1341 } 1342 return &asset.Contract{ 1343 Coin: contract, 1344 SwapAddress: receiver.String(), 1345 ContractData: contract.redeemScript, 1346 SecretHash: secretHash, 1347 LockTime: time.Unix(int64(lockTime), 0), 1348 TxData: contract.tx.raw, 1349 }, nil 1350 } 1351 1352 // run is responsible for best block polling and checking the application 1353 // context to trigger a clean shutdown. 1354 func (btc *Backend) run(ctx context.Context) { 1355 defer btc.shutdown() 1356 1357 if blockPollInterval == 0 { 1358 blockPollInterval = time.Second 1359 if btc.rpcCfg.IsPublicProvider { 1360 blockPollInterval = time.Second * 10 1361 } 1362 } 1363 1364 btc.log.Infof("Starting %v block polling with interval of %v", 1365 strings.ToUpper(btc.name), blockPollInterval) 1366 blockPoll := time.NewTicker(blockPollInterval) 1367 defer blockPoll.Stop() 1368 addBlock := func(block *GetBlockVerboseResult, reorg bool) { 1369 _, err := btc.blockCache.add(block) 1370 if err != nil { 1371 btc.log.Errorf("error adding new best block to cache: %v", err) 1372 } 1373 btc.signalMtx.RLock() 1374 btc.log.Tracef("Notifying %d %s asset consumers of new block at height %d", 1375 len(btc.blockChans), btc.name, block.Height) 1376 for c := range btc.blockChans { 1377 select { 1378 case c <- &asset.BlockUpdate{ 1379 Err: nil, 1380 Reorg: reorg, 1381 }: 1382 default: 1383 btc.log.Errorf("failed to send block update on blocking channel") 1384 // Commented to try sends on future blocks. 1385 // close(c) 1386 // delete(btc.blockChans, c) 1387 // 1388 // TODO: Allow the receiver (e.g. Swapper.Run) to inform done 1389 // status so the channels can be retired cleanly rather than 1390 // trying them forever. 1391 } 1392 } 1393 btc.signalMtx.RUnlock() 1394 } 1395 1396 sendErr := func(err error) { 1397 btc.log.Error(err) 1398 for c := range btc.blockChans { 1399 select { 1400 case c <- &asset.BlockUpdate{ 1401 Err: err, 1402 }: 1403 default: 1404 btc.log.Errorf("failed to send sending block update on blocking channel") 1405 // close(c) 1406 // delete(btc.blockChans, c) 1407 } 1408 } 1409 } 1410 1411 sendErrFmt := func(s string, a ...any) { 1412 sendErr(fmt.Errorf(s, a...)) 1413 } 1414 1415 out: 1416 for { 1417 select { 1418 case <-blockPoll.C: 1419 tip := btc.blockCache.tip() 1420 bestHash, err := btc.node.GetBestBlockHash() 1421 if err != nil { 1422 sendErr(asset.NewConnectionError("error retrieving best block: %v", err)) 1423 continue 1424 } 1425 if *bestHash == tip.hash { 1426 continue 1427 } 1428 best := bestHash.String() 1429 block, err := btc.node.GetBlockVerbose(bestHash) 1430 if err != nil { 1431 sendErrFmt("error retrieving block %s: %v", best, err) 1432 continue 1433 } 1434 // If this doesn't build on the best known block, look for a reorg. 1435 prevHash, err := chainhash.NewHashFromStr(block.PreviousHash) 1436 if err != nil { 1437 sendErrFmt("error parsing previous hash %s: %v", block.PreviousHash, err) 1438 continue 1439 } 1440 // If it builds on the best block or the cache is empty, it's good to add. 1441 if *prevHash == tip.hash || tip.height == 0 { 1442 btc.log.Debugf("New block %s (%d)", bestHash, block.Height) 1443 addBlock(block, false) 1444 continue 1445 } 1446 // It is either a reorg, or the previous block is not the cached 1447 // best block. Crawl blocks backwards until finding a mainchain 1448 // block, flagging blocks from the cache as orphans along the way. 1449 iHash := &tip.hash 1450 reorgHeight := int64(0) 1451 for { 1452 if *iHash == zeroHash { 1453 break 1454 } 1455 iBlock, err := btc.node.GetBlockVerbose(iHash) 1456 if err != nil { 1457 sendErrFmt("error retrieving block %s: %v", iHash, err) 1458 break 1459 } 1460 if iBlock.Confirmations > -1 { 1461 // This is a mainchain block, nothing to do. 1462 break 1463 } 1464 if iBlock.Height == 0 { 1465 break 1466 } 1467 reorgHeight = iBlock.Height 1468 iHash, err = chainhash.NewHashFromStr(iBlock.PreviousHash) 1469 if err != nil { 1470 sendErrFmt("error decoding previous hash %s for block %s: %v", 1471 iBlock.PreviousHash, iHash.String(), err) 1472 // Some blocks on the side chain may not be flagged as 1473 // orphaned, but still proceed, flagging the ones we have 1474 // identified and adding the new best block to the cache and 1475 // setting it to the best block in the cache. 1476 break 1477 } 1478 } 1479 var reorg bool 1480 if reorgHeight > 0 { 1481 reorg = true 1482 btc.log.Infof("Reorg from %s (%d) to %s (%d) detected.", 1483 tip.hash, tip.height, bestHash, block.Height) 1484 btc.blockCache.reorg(reorgHeight) 1485 } 1486 // Now add the new block. 1487 addBlock(block, reorg) 1488 case <-ctx.Done(): 1489 break out 1490 } 1491 } 1492 } 1493 1494 // estimateFee attempts to get a reasonable tx fee rates (units: atomic/(v)byte) 1495 // to use for the asset by checking estimate(smart)fee. That call can fail or 1496 // otherwise be useless on an otherwise perfectly functioning node. In that 1497 // case, an estimate is calculated from the median fees of the previous 1498 // block(s). 1499 func (btc *Backend) estimateFee(ctx context.Context) (satsPerB uint64, err error) { 1500 if btc.cfg.FeeFetcher != nil { 1501 const feeRateExpiry = time.Minute * 10 1502 btc.feeRateCache.RLock() 1503 stamp, feeRate := btc.feeRateCache.stamp, btc.feeRateCache.feeRate 1504 btc.feeRateCache.RUnlock() 1505 if time.Since(stamp) < feeRateExpiry { 1506 return feeRate, nil 1507 } else { 1508 btc.log.Warnf("external btc fee rate is expired. falling back to estimatesmartfee") 1509 } 1510 } 1511 1512 if btc.cfg.DumbFeeEstimates { 1513 satsPerB, err = btc.node.EstimateFee(btc.feeConfs) 1514 } else { 1515 satsPerB, err = btc.node.EstimateSmartFee(btc.feeConfs, &btcjson.EstimateModeConservative) 1516 } 1517 if err == nil && satsPerB > 0 { 1518 return satsPerB, nil 1519 } else if err != nil && !errors.Is(err, errNoFeeRate) { 1520 btc.log.Debugf("Estimate fee failure: %v", err) 1521 } 1522 btc.log.Debugf("No fee estimate from node. Computing median fee rate from blocks...") 1523 1524 tip := btc.blockCache.tipHash() 1525 1526 btc.feeCache.Lock() 1527 defer btc.feeCache.Unlock() 1528 1529 // If the current block hasn't changed, no need to recalc. 1530 if btc.feeCache.hash == tip { 1531 return btc.feeCache.fee, nil 1532 } 1533 1534 // Need to revert to the median fee calculation. 1535 if btc.cfg.ManualMedianFee { 1536 satsPerB, err = btc.node.medianFeesTheHardWay(ctx) 1537 } else { 1538 satsPerB, err = btc.node.medianFeeRate() 1539 } 1540 if err != nil { 1541 if errors.Is(err, errNoCompetition) { 1542 btc.log.Debugf("Blocks are too empty to calculate median fees. "+ 1543 "Using no-competition rate (%d).", btc.noCompetitionRate) 1544 btc.feeCache.fee = btc.noCompetitionRate 1545 btc.feeCache.hash = tip 1546 return btc.noCompetitionRate, nil 1547 } 1548 return 0, err 1549 } 1550 if satsPerB < btc.noCompetitionRate { 1551 btc.log.Debugf("Calculated median fees %d are lower than the no-competition rate %d. Using the latter.", 1552 satsPerB, btc.noCompetitionRate) 1553 satsPerB = btc.noCompetitionRate 1554 } 1555 btc.feeCache.fee = satsPerB 1556 btc.feeCache.hash = tip 1557 return satsPerB, nil 1558 } 1559 1560 // decodeCoinID decodes the coin ID into a tx hash and a vout. 1561 func decodeCoinID(coinID []byte) (*chainhash.Hash, uint32, error) { 1562 if len(coinID) != 36 { 1563 return nil, 0, fmt.Errorf("coin ID wrong length. expected 36, got %d", len(coinID)) 1564 } 1565 var txHash chainhash.Hash 1566 copy(txHash[:], coinID[:32]) 1567 return &txHash, binary.BigEndian.Uint32(coinID[32:]), nil 1568 } 1569 1570 // toCoinID converts the outpoint to a coin ID. 1571 func toCoinID(txHash *chainhash.Hash, vout uint32) []byte { 1572 hashLen := len(txHash) 1573 b := make([]byte, hashLen+4) 1574 copy(b[:hashLen], txHash[:]) 1575 binary.BigEndian.PutUint32(b[hashLen:], vout) 1576 return b 1577 } 1578 1579 // Convert the BTC value to satoshis. 1580 func toSat(v float64) uint64 { 1581 return uint64(math.Round(v * conventionalConversionFactor)) 1582 } 1583 1584 // isTxNotFoundErr will return true if the error indicates that the requested 1585 // transaction is not known. 1586 func isTxNotFoundErr(err error) bool { 1587 // We are using dcrd's client with Bitcoin Core, so errors will be of type 1588 // dcrjson.RPCError, but numeric codes should come from btcjson. 1589 const errRPCNoTxInfo = int(btcjson.ErrRPCNoTxInfo) 1590 var rpcErr *dcrjson.RPCError 1591 return errors.As(err, &rpcErr) && int(rpcErr.Code) == errRPCNoTxInfo 1592 } 1593 1594 // isMethodNotFoundErr will return true if the error indicates that the RPC 1595 // method was not found by the RPC server. The error must be dcrjson.RPCError 1596 // with a numeric code equal to btcjson.ErrRPCMethodNotFound.Code or a message 1597 // containing "method not found". 1598 func isMethodNotFoundErr(err error) bool { 1599 var errRPCMethodNotFound = int(btcjson.ErrRPCMethodNotFound.Code) 1600 var rpcErr *dcrjson.RPCError 1601 return errors.As(err, &rpcErr) && 1602 (int(rpcErr.Code) == errRPCMethodNotFound || 1603 strings.Contains(strings.ToLower(rpcErr.Message), "method not found")) 1604 } 1605 1606 // msgTxFromBytes creates a wire.MsgTx by deserializing the transaction. 1607 // WARNING: You probably want to use a deserializer that is appropriate for the 1608 // asset instead. This does not work for all assets, like Zcash. 1609 func msgTxFromBytes(txB []byte) (*wire.MsgTx, error) { 1610 msgTx := new(wire.MsgTx) 1611 if err := msgTx.Deserialize(bytes.NewReader(txB)); err != nil { 1612 return nil, err 1613 } 1614 return msgTx, nil 1615 } 1616 1617 // hashTx just calls the tx's TxHash method. 1618 // WARNING: You probably want to use a hasher that is appropriate for the 1619 // asset instead. This does not work for all assets, like Zcash. 1620 func hashTx(tx *wire.MsgTx) *chainhash.Hash { 1621 h := tx.TxHash() 1622 return &h 1623 } 1624 1625 var freeFeeSources = []*txfee.SourceConfig{ 1626 { // https://mempool.space/docs/api/rest#get-recommended-fees 1627 Name: "mempool.space", 1628 Rank: 1, 1629 Period: time.Minute * 2, // Rate limit might be 1 per 10 seconds. 1630 F: func(ctx context.Context) (rate uint64, errDelay time.Duration, err error) { 1631 const uri = "https://mempool.space/api/v1/fees/recommended" 1632 var res struct { 1633 FastestFee uint64 `json:"fastestFee"` 1634 } 1635 var code int 1636 if err := dexnet.Get(ctx, uri, &res, dexnet.WithStatusFunc(func(respCode int) { 1637 code = respCode 1638 })); err != nil { 1639 if code == http.StatusTooManyRequests { // 429 per docs 1640 return 0, time.Minute * 30, errors.New("exceeded request limit") 1641 } 1642 return 0, time.Minute * 2, err 1643 } 1644 return res.FastestFee, 0, nil 1645 }, 1646 }, 1647 { // https://bitcoiner.live/doc/api 1648 Name: "bitcoiner.live", 1649 Rank: 1, 1650 Period: time.Minute * 5, // Data is refreshed every 5 minutes 1651 F: func(ctx context.Context) (rate uint64, errDelay time.Duration, err error) { 1652 const uri = "https://bitcoiner.live/api/fees/estimates/latest" 1653 var res struct { 1654 Estimates map[string]struct { 1655 SatsPerVB float64 `json:"sat_per_vbyte"` 1656 } `json:"estimates"` 1657 } 1658 if err := dexnet.Get(ctx, uri, &res); err != nil { 1659 return 0, time.Minute * 10, err 1660 } 1661 if res.Estimates == nil { 1662 return 0, time.Minute * 10, errors.New("no estimates returned") 1663 } 1664 // Using 30 minutes estimate. There is also a 60, 120, and higher 1665 r, found := res.Estimates["30"] 1666 if !found { 1667 return 0, time.Minute * 10, errors.New("no 30-minute estimate returned") 1668 } 1669 return uint64(math.Round(r.SatsPerVB)), 0, nil 1670 }, 1671 }, 1672 { 1673 // https://api.blockcypher.com/v1/btc/main 1674 // Also have ltc, dash, doge 1675 Name: "blockcypher.com", 1676 Rank: 1, 1677 Period: time.Minute * 3, // 100 requests/hr => 0.6 minutes 1678 F: func(ctx context.Context) (rate uint64, errDelay time.Duration, err error) { 1679 const uri = "https://api.blockcypher.com/v1/btc/main" 1680 var res struct { 1681 MediumPerKB uint64 `json:"medium_fee_per_kb"` 1682 } 1683 var code int 1684 if err := dexnet.Get(ctx, uri, &res, dexnet.WithStatusFunc(func(respCode int) { 1685 code = respCode 1686 })); err != nil { 1687 if code == http.StatusTooManyRequests { // 429 per docs 1688 // There's a X-Ratelimit-Remaining response header that 1689 // could potentially be used to caculate a proper delay here. 1690 return 0, time.Minute * 30, errors.New("exceeded request limit") 1691 } 1692 return 0, time.Minute * 10, err 1693 } 1694 return uint64(math.Round(float64(res.MediumPerKB) / 1e3)), 0, nil 1695 }, 1696 }, 1697 { // undocumented 1698 Name: "btc.com", 1699 Rank: 2, 1700 Period: time.Minute * 5, 1701 F: func(ctx context.Context) (rate uint64, errDelay time.Duration, err error) { 1702 const uri = "https://btc.com/service/fees/distribution" 1703 var res struct { 1704 RecommendedFees struct { 1705 OneBlockFee uint64 `json:"one_block_fee"` 1706 } `json:"fees_recommended"` 1707 } 1708 if err := dexnet.Get(ctx, uri, &res); err != nil { 1709 return 0, time.Minute * 10, err 1710 } 1711 return res.RecommendedFees.OneBlockFee, 0, nil 1712 }, 1713 }, 1714 { // undocumented. source is somehow related to blockchain.com 1715 Name: "blockchain.info", 1716 Rank: 2, 1717 Period: time.Minute * 3, // Rate limit might be 1 per 10 seconds. 1718 F: func(ctx context.Context) (rate uint64, errDelay time.Duration, err error) { 1719 const uri = "https://api.blockchain.info/mempool/fees" 1720 var res struct { 1721 Regular uint64 `json:"regular"` // Might be a little low 1722 Priority uint64 `json:"priority"` 1723 } 1724 if err := dexnet.Get(ctx, uri, &res); err != nil { 1725 return 0, time.Minute * 10, err 1726 } 1727 return res.Priority, 0, nil 1728 }, 1729 }, 1730 { 1731 // undocumented. Probably just estimatesmartfee underneath 1732 Name: "bitcoinfees.net", 1733 Rank: 2, 1734 Period: time.Minute * 3, 1735 F: func(ctx context.Context) (rate uint64, errDelay time.Duration, err error) { 1736 const uri = "https://bitcoinfees.net/api.json" 1737 var res struct { 1738 FeePerKBByBlockTarget map[string]uint64 `json:"fee_by_block_target"` 1739 } 1740 if err := dexnet.Get(ctx, uri, &res); err != nil { 1741 return 0, time.Minute * 10, err 1742 } 1743 if res.FeePerKBByBlockTarget == nil { 1744 return 0, time.Minute * 10, errors.New("no estimates returned") 1745 } 1746 // Using 30 minutes estimate. There is also a 60, 120, and higher 1747 r, found := res.FeePerKBByBlockTarget["1"] 1748 if !found { 1749 return 0, time.Minute * 10, errors.New("no 1-block estimate returned") 1750 } 1751 return uint64(math.Round(float64(r) / 1e3)), 0, nil 1752 }, 1753 }, 1754 { 1755 // https://blockchair.com/api/docs#link_M0 1756 Name: "blockchair.com", 1757 Rank: 3, // blockchair sometimes returns zero. Use only as a last resort. 1758 Period: time.Minute * 3, // 1440 per day => 1 request / minute 1759 F: func(ctx context.Context) (rate uint64, errDelay time.Duration, err error) { 1760 const uri = "https://api.blockchair.com/bitcoin/stats" 1761 var res struct { 1762 Data struct { 1763 SatsPerByte uint64 `json:"suggested_transaction_fee_per_byte_sat"` 1764 } `json:"data"` 1765 } 1766 var code int 1767 if err := dexnet.Get(ctx, uri, &res, dexnet.WithStatusFunc(func(respCode int) { 1768 code = respCode 1769 })); err != nil { 1770 switch code { 1771 case http.StatusTooManyRequests, http.StatusPaymentRequired: 1772 return 0, time.Minute * 30, errors.New("exceeded request limit") 1773 case http.StatusServiceUnavailable, 430, 434: 1774 return 0, time.Hour * 24, errors.New("banned from api") 1775 } 1776 return 0, time.Minute * 10, err 1777 } 1778 return res.Data.SatsPerByte, 0, nil 1779 }, 1780 }, 1781 } 1782 1783 func tatumFeeFetcher(apiKey string) *txfee.SourceConfig { 1784 return &txfee.SourceConfig{ 1785 Name: "tatum", 1786 Rank: 1, 1787 Period: time.Minute * 1, // 1M credit / mo => 3 req / sec 1788 F: func(ctx context.Context) (rate uint64, errDelay time.Duration, err error) { 1789 const uri = "https://api.tatum.io/v3/blockchain/fee/BTC" 1790 var res struct { 1791 Fast float64 `json:"fast"` // Might be a little high 1792 Medium float64 `json:"medium"` 1793 } 1794 var code int 1795 withCode := dexnet.WithStatusFunc(func(respCode int) { 1796 code = respCode 1797 }) 1798 withApiKey := dexnet.WithRequestHeader("x-api-key", apiKey) 1799 if err := dexnet.Get(ctx, uri, &res, withCode, withApiKey); err != nil { 1800 if code == http.StatusForbidden { 1801 return 0, time.Minute * 30, errors.New("exceeded request limit") 1802 } 1803 return 0, time.Minute * 10, err 1804 } 1805 return uint64(math.Round(res.Medium)), 0, nil 1806 }, 1807 } 1808 } 1809 1810 func blockDaemonFeeFetcher(apiKey string) *txfee.SourceConfig { 1811 // https://docs.blockdaemon.com/reference/getfeeestimate 1812 return &txfee.SourceConfig{ 1813 Name: "blockdaemon", 1814 Rank: 1, 1815 Period: time.Minute * 2, // 25 reqs/second, 3M compute units, request is 50 compute units => 1 req / 43 secs 1816 F: func(ctx context.Context) (rate uint64, errDelay time.Duration, err error) { 1817 const uri = "https://svc.blockdaemon.com/universal/v1/bitcoin/mainnet/tx/estimate_fee" 1818 var res struct { 1819 Fees struct { 1820 Fast uint64 `json:"fast"` // a little high 1821 Medium uint64 `json:"medium"` 1822 } `json:"estimated_fees"` 1823 } 1824 var code int 1825 withCode := dexnet.WithStatusFunc(func(respCode int) { 1826 code = respCode 1827 }) 1828 withApiKey := dexnet.WithRequestHeader("X-API-Key", apiKey) 1829 if err := dexnet.Get(ctx, uri, &res, withCode, withApiKey); err != nil { 1830 if code == http.StatusTooManyRequests { 1831 return 0, time.Minute * 30, errors.New("exceeded request limit") 1832 } 1833 return 0, time.Minute * 10, err 1834 } 1835 return res.Fees.Medium, 0, nil 1836 }, 1837 } 1838 }